Skip to content

Commit 16318fb

Browse files
Fix: Make plan mode work via prompt instruction (shepherdjerred#171)
* fix: prepend plan mode instruction to prompt when enabled The plan mode checkbox now prepends "Enter plan mode and create a plan before doing anything." to the user's prompt instead of relying on the --permission-mode plan flag. This is more reliable because: - Works regardless of --dangerously-skip-permissions flag - Directly instructs Claude via prompt rather than CLI flag - Avoids compatibility issues with Claude Code's flag parsing The --permission-mode plan flag doesn't work when combined with --dangerously-skip-permissions, so prompt-based instructions are the better approach. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: address PR feedback - Extract prompt-prepending logic into CreateDialogState::build_initial_prompt() helper - Remove --permission-mode plan flag from backends (doesn't work with --dangerously-skip-permissions) - Plan mode is now handled purely via prompt instruction - Remove obsolete plan mode tests from backends - Trim user prompts to avoid extra newlines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix: correct test formatting after parameter removal The Python script that removed plan_mode parameter left some test calls with malformed formatting (comments and arrays on same line). This fixes the syntax errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: clarify that plan_mode field is not used by backends Updated comments to reflect that the plan_mode field in CreateOptions is kept for protocol compatibility but is not actually used by backends. Plan mode activation is now handled via prompt instruction prepending in the TUI layer. Also updated the deprecated create_container() method comment to clarify that the plan_mode field is unused. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: move plan mode prompt transformation to manager layer This is a better architectural decision that: - Works for ALL session creation paths (TUI, API, CLI, etc.) - Actually uses the plan_mode flag in CreateOptions - Centralizes the logic in one place (manager/daemon layer) - TUI no longer needs to know implementation details Changes: - Removed build_initial_prompt() helper from TUI CreateDialogState - TUI now passes prompt as-is to the API/daemon - Manager prepends plan mode instruction when plan_mode=true - Updated documentation to reflect that plan_mode IS used This ensures plan mode works consistently regardless of how the session is created. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fe53dfd commit 16318fb

4 files changed

Lines changed: 23 additions & 140 deletions

File tree

packages/multiplexer/src/backends/docker.rs

Lines changed: 1 addition & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ impl DockerBackend {
160160
/// * `print_mode` - If true, run in non-interactive mode with `--print --verbose` flags.
161161
/// The container will output the response and exit.
162162
/// If false, run interactively for `docker attach`.
163-
/// * `plan_mode` - If true, add `--permission-mode plan` flag to start in plan mode.
164163
///
165164
/// # Errors
166165
///
@@ -172,7 +171,6 @@ impl DockerBackend {
172171
uid: u32,
173172
proxy_config: Option<&DockerProxyConfig>,
174173
print_mode: bool,
175-
plan_mode: bool,
176174
images: &[String],
177175
) -> anyhow::Result<Vec<String>> {
178176
let container_name = format!("mux-{name}");
@@ -402,11 +400,6 @@ impl DockerBackend {
402400
let claude_cmd = {
403401
let mut cmd = "claude --dangerously-skip-permissions".to_string();
404402

405-
// Add plan mode flag if enabled
406-
if plan_mode {
407-
cmd.push_str(" --permission-mode plan");
408-
}
409-
410403
// Add print mode flags
411404
if print_mode {
412405
cmd.push_str(" --print --verbose");
@@ -491,7 +484,6 @@ impl ExecutionBackend for DockerBackend {
491484
uid,
492485
proxy_config_ref,
493486
options.print_mode,
494-
options.plan_mode,
495487
&options.images,
496488
)?;
497489
let output = Command::new("docker")
@@ -644,7 +636,6 @@ mod tests {
644636
1000,
645637
None,
646638
false, // interactive mode
647-
false, // plan mode
648639
&[], // no images
649640
).expect("Failed to build args");
650641

@@ -671,7 +662,6 @@ mod tests {
671662
uid,
672663
None,
673664
false, // print mode
674-
false, // plan mode
675665
&[], // no images
676666
).expect("Failed to build args");
677667

@@ -735,7 +725,6 @@ mod tests {
735725
1000,
736726
None,
737727
false, // print mode
738-
false, // plan mode
739728
&[], // no images
740729
).expect("Failed to build args");
741730

@@ -759,7 +748,6 @@ mod tests {
759748
1000,
760749
None,
761750
false, // print mode
762-
false, // plan mode
763751
&[], // no images
764752
).expect("Failed to build args");
765753

@@ -799,7 +787,6 @@ mod tests {
799787
1000,
800788
Some(&proxy_config),
801789
false, // print mode
802-
false, // plan mode
803790
&[], // no images
804791
).expect("Failed to build args");
805792

@@ -837,7 +824,6 @@ mod tests {
837824
1000,
838825
Some(&proxy_config),
839826
false, // print mode
840-
false, // plan mode
841827
&[], // no images
842828
).expect("Failed to build args");
843829

@@ -861,7 +847,6 @@ mod tests {
861847
1000,
862848
Some(&proxy_config),
863849
false, // print mode
864-
false, // plan mode
865850
&[], // no images
866851
).expect("Failed to build args");
867852

@@ -887,7 +872,6 @@ mod tests {
887872
1000,
888873
Some(&proxy_config),
889874
false, // print mode
890-
false, // plan mode
891875
&[], // no images
892876
).expect("Failed to build args");
893877

@@ -913,8 +897,7 @@ mod tests {
913897
"test prompt",
914898
1000,
915899
None,
916-
true, // print mode enabled
917-
false, // plan mode
900+
true, // print mode
918901
&[], // no images
919902
).expect("Failed to build args");
920903

@@ -939,7 +922,6 @@ mod tests {
939922
1000,
940923
None,
941924
false, // interactive mode
942-
false, // plan mode
943925
&[], // no images
944926
).expect("Failed to build args");
945927

@@ -1181,74 +1163,4 @@ mod tests {
11811163
);
11821164
}
11831165

1184-
/// Test that plan mode adds --permission-mode plan flag
1185-
#[test]
1186-
fn test_plan_mode_adds_flag() {
1187-
let args = DockerBackend::build_create_args(
1188-
"test-session",
1189-
&PathBuf::from("/workspace"),
1190-
"test prompt",
1191-
1000,
1192-
None,
1193-
false, // print mode
1194-
true, // plan mode enabled
1195-
&[], // no images
1196-
).expect("Failed to build args");
1197-
1198-
let cmd_arg = args.last().unwrap();
1199-
assert!(
1200-
cmd_arg.contains("--permission-mode plan"),
1201-
"Plan mode should add --permission-mode plan flag: {cmd_arg}"
1202-
);
1203-
}
1204-
1205-
/// Test that plan mode with print mode includes both flags
1206-
#[test]
1207-
fn test_plan_mode_with_print_mode() {
1208-
let args = DockerBackend::build_create_args(
1209-
"test-session",
1210-
&PathBuf::from("/workspace"),
1211-
"test prompt",
1212-
1000,
1213-
None,
1214-
true, // print mode enabled
1215-
true, // plan mode enabled
1216-
&[], // no images
1217-
).expect("Failed to build args");
1218-
1219-
let cmd_arg = args.last().unwrap();
1220-
assert!(
1221-
cmd_arg.contains("--permission-mode plan"),
1222-
"Should include --permission-mode plan: {cmd_arg}"
1223-
);
1224-
assert!(
1225-
cmd_arg.contains("--print"),
1226-
"Should include --print: {cmd_arg}"
1227-
);
1228-
assert!(
1229-
cmd_arg.contains("--verbose"),
1230-
"Should include --verbose: {cmd_arg}"
1231-
);
1232-
}
1233-
1234-
/// Test that plan mode disabled does not add permission-mode flag
1235-
#[test]
1236-
fn test_plan_mode_disabled() {
1237-
let args = DockerBackend::build_create_args(
1238-
"test-session",
1239-
&PathBuf::from("/workspace"),
1240-
"test prompt",
1241-
1000,
1242-
None,
1243-
false, // print mode
1244-
false, // plan mode disabled
1245-
&[], // no images
1246-
).expect("Failed to build args");
1247-
1248-
let cmd_arg = args.last().unwrap();
1249-
assert!(
1250-
!cmd_arg.contains("--permission-mode"),
1251-
"Should not include --permission-mode when disabled: {cmd_arg}"
1252-
);
1253-
}
12541166
}

packages/multiplexer/src/backends/traits.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub struct CreateOptions {
3939
pub print_mode: bool,
4040

4141
/// Start in plan mode (read-only exploration).
42-
/// Applicable to both Zellij and Docker backends.
42+
/// When enabled, the manager prepends instructions to the prompt before passing to backends.
4343
pub plan_mode: bool,
4444

4545
/// Session-specific proxy port (overrides global proxy port).

packages/multiplexer/src/backends/zellij.rs

Lines changed: 12 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,12 @@ impl ZellijBackend {
2626

2727
/// Build the args for running Claude in a new pane (exposed for testing)
2828
#[must_use]
29-
pub fn build_new_pane_args(workdir: &Path, initial_prompt: &str, plan_mode: bool, images: &[String]) -> Vec<String> {
29+
pub fn build_new_pane_args(workdir: &Path, initial_prompt: &str, images: &[String]) -> Vec<String> {
3030
let escaped_prompt = initial_prompt.replace('\'', "'\\''");
3131

32-
// Build claude command with plan_mode and images
32+
// Build claude command with images
3333
let mut claude_cmd = "claude --dangerously-skip-permissions".to_string();
3434

35-
// Add plan mode flag if enabled
36-
if plan_mode {
37-
claude_cmd.push_str(" --permission-mode plan");
38-
}
39-
4035
// Add image arguments
4136
for image in images {
4237
let escaped_image = image.replace('\'', "'\\''");
@@ -104,7 +99,7 @@ impl ExecutionBackend for ZellijBackend {
10499
}
105100

106101
// Run Claude in the session
107-
let pane_args = Self::build_new_pane_args(workdir, initial_prompt, options.plan_mode, &options.images);
102+
let pane_args = Self::build_new_pane_args(workdir, initial_prompt, &options.images);
108103
let output = Command::new("zellij")
109104
.args(&pane_args[..])
110105
.env("ZELLIJ_SESSION_NAME", name)
@@ -265,7 +260,7 @@ mod tests {
265260
#[test]
266261
fn test_new_pane_has_cwd() {
267262
let workdir = PathBuf::from("/my/work/dir");
268-
let args = ZellijBackend::build_new_pane_args(&workdir, "test prompt", false, &[]);
263+
let args = ZellijBackend::build_new_pane_args(&workdir, "test prompt", &[]);
269264

270265
assert!(
271266
args.contains(&"--cwd".to_string()),
@@ -283,7 +278,7 @@ mod tests {
283278
/// Test that new-pane uses action subcommand
284279
#[test]
285280
fn test_new_pane_uses_action() {
286-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &[]);
281+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &[]);
287282

288283
assert_eq!(args[0], "action", "Expected 'action' as first arg");
289284
assert_eq!(args[1], "new-pane", "Expected 'new-pane' as second arg");
@@ -293,7 +288,7 @@ mod tests {
293288
#[test]
294289
fn test_prompt_escaping() {
295290
let prompt_with_quotes = "Say 'hello world'";
296-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), prompt_with_quotes, false, &[]);
291+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), prompt_with_quotes, &[]);
297292

298293
// Find the command argument (last one containing the prompt)
299294
let cmd_arg = args.last().unwrap();
@@ -319,7 +314,7 @@ mod tests {
319314
/// Test that new-pane command uses bash shell
320315
#[test]
321316
fn test_new_pane_uses_bash() {
322-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &[]);
317+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &[]);
323318

324319
assert!(
325320
args.contains(&"bash".to_string()),
@@ -330,7 +325,7 @@ mod tests {
330325
/// Test that new-pane includes -- separator before command
331326
#[test]
332327
fn test_new_pane_has_separator() {
333-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &[]);
328+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &[]);
334329

335330
assert!(
336331
args.contains(&"--".to_string()),
@@ -346,7 +341,7 @@ mod tests {
346341
/// Test that command includes claude with --dangerously-skip-permissions
347342
#[test]
348343
fn test_command_includes_dangerous_flag() {
349-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &[]);
344+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &[]);
350345

351346
let cmd_arg = args.last().unwrap();
352347
assert!(
@@ -359,7 +354,7 @@ mod tests {
359354
#[test]
360355
fn test_command_includes_images() {
361356
let images = vec!["/path/to/image1.png".to_string(), "/path/to/image2.jpg".to_string()];
362-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &images);
357+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &images);
363358

364359
let cmd_arg = args.last().unwrap();
365360
assert!(
@@ -376,7 +371,7 @@ mod tests {
376371
#[test]
377372
fn test_image_path_escaping() {
378373
let images = vec!["/path/with'quote/image.png".to_string()];
379-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &images);
374+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &images);
380375

381376
let cmd_arg = args.last().unwrap();
382377
// Single quotes should be escaped as '\'' for shell safety (end string, escaped quote, start string)
@@ -389,7 +384,7 @@ mod tests {
389384
/// Test that command works with no images
390385
#[test]
391386
fn test_command_with_no_images() {
392-
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", false, &[]);
387+
let args = ZellijBackend::build_new_pane_args(&PathBuf::from("/workspace"), "test prompt", &[]);
393388

394389
let cmd_arg = args.last().unwrap();
395390
assert!(
@@ -402,35 +397,4 @@ mod tests {
402397
);
403398
}
404399

405-
/// Test that plan mode adds --permission-mode plan flag
406-
#[test]
407-
fn test_plan_mode_adds_flag() {
408-
let args = ZellijBackend::build_new_pane_args(
409-
&PathBuf::from("/workspace"),
410-
"test prompt",
411-
true, // plan_mode = true
412-
&[], // no images
413-
);
414-
let cmd_arg = args.last().unwrap();
415-
assert!(
416-
cmd_arg.contains("--permission-mode plan"),
417-
"Plan mode should add --permission-mode plan flag: {cmd_arg}"
418-
);
419-
}
420-
421-
/// Test that plan mode disabled does not add permission-mode flag
422-
#[test]
423-
fn test_plan_mode_disabled() {
424-
let args = ZellijBackend::build_new_pane_args(
425-
&PathBuf::from("/workspace"),
426-
"test prompt",
427-
false, // plan_mode = false
428-
&[], // no images
429-
);
430-
let cmd_arg = args.last().unwrap();
431-
assert!(
432-
!cmd_arg.contains("--permission-mode"),
433-
"Non-plan mode should not include --permission-mode flag: {cmd_arg}"
434-
);
435-
}
436400
}

packages/multiplexer/src/core/manager.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ impl SessionManager {
233233
None
234234
};
235235

236+
// Prepend plan mode instruction if enabled
237+
let transformed_prompt = if plan_mode {
238+
format!("Enter plan mode and create a plan before doing anything.\n\n{}", initial_prompt.trim())
239+
} else {
240+
initial_prompt.clone()
241+
};
242+
236243
// Create backend resource
237244
let create_options = crate::backends::CreateOptions {
238245
print_mode,
@@ -243,12 +250,12 @@ impl SessionManager {
243250
let backend_id = match backend {
244251
BackendType::Zellij => {
245252
self.zellij
246-
.create(&full_name, &worktree_path, &initial_prompt, create_options)
253+
.create(&full_name, &worktree_path, &transformed_prompt, create_options)
247254
.await?
248255
}
249256
BackendType::Docker => {
250257
self.docker
251-
.create(&full_name, &worktree_path, &initial_prompt, create_options)
258+
.create(&full_name, &worktree_path, &transformed_prompt, create_options)
252259
.await?
253260
}
254261
};

0 commit comments

Comments
 (0)