298
message
299
}
300
301
+/// The full system prompt for a TUI backend: the model-appropriate base prompt
302
+/// plus [`session_context_message`] for the launch directory. The TUI
303
+/// counterpart of the ACP entry points, which push that context as a separate
304
+/// system message — without the cwd guidance the model doesn't know where the
305
+/// project lives and invents placeholder paths (e.g. `/init` trying to write
306
+/// `/path/to/repo/AGENTS.md`).
307
+#[cfg(unix)]
308
+pub(crate) fn tui_system_prompt(tool_calling: bool) -> String {
309
+ let base = system_prompt_for_model(tool_calling);
310
+ match std::env::current_dir() {
311
+ Ok(cwd) => format!("{base}\n\n{}", session_context_message(&cwd)),
312
+ Err(_) => base.to_string(),
313
+ }
314
+}
315
+
316
fn agent_tools_as_specs() -> Vec<ToolSpec> {
317
let mut specs: Vec<ToolSpec> = tools::all_tools()
318
.into_iter()
2937
// channel so the loading-phase plumbing in `chat::run_with` is unchanged.
2938
let (load_tx, load_rx) = std::sync::mpsc::channel::<Result<(), String>>();
2939
2925
- // Project instruction files (AGENTS.md / CLAUDE.md) for the launch directory,
2926
- // injected into the system prompt so the TUI shares the same always-on
2927
- // project context the ACP sessions get.
2928
- let project_instructions = std::env::current_dir()
2929
- .ok()
2930
- .and_then(|cwd| instructions::load_project_instructions(&cwd));
2931
- let with_instructions = |base: String| match &project_instructions {
2932
- Some(extra) => format!("{base}\n\n{extra}"),
2933
- None => base,
2934
- };
2940
+ // Session context (cwd guidance + AGENTS.md / CLAUDE.md instruction files)
2941
+ // for the launch directory, injected into the system prompt so the TUI
2942
+ // shares the same always-on project context the ACP sessions get — in
2943
+ // particular the model must know the real working directory, or tools like
2944
+ // `/init` write to invented paths.
2945
+ let remote_system_prompt = tui_system_prompt(true);
2946
2947
// Pick the inference backend: a configured provider if present, else on-device.
2948
let (inference_backend, startup_model_name): (Arc<dyn InferenceBackend>, String) =
2962
provider.base_url,
2963
provider.api_key,
2964
provider.model,
2954
- Some(with_instructions(SYSTEM_PROMPT.to_string())),
2965
+ Some(remote_system_prompt.clone()),
2966
)) as Arc<dyn InferenceBackend>;
2967
(backend, label)
2968
}
2970
// Honor the Local Inference toggle: when off and signed in, start
2971
// on a cloud tier. Otherwise bring up on-device WITHOUT loading a
2972
// model — the user loads it explicitly with /load (or /models), so
2962
- // the UI comes up immediately. Project instructions are injected at
2973
+ // the UI comes up immediately. Session context is injected at
2974
// load time in `chat.rs`.
2975
let cloud_when_off = if settings::local_inference_enabled() {
2976
None
2992
provider.base_url,
2993
provider.api_key,
2994
provider.model,
2984
- Some(with_instructions(SYSTEM_PROMPT.to_string())),
2995
+ Some(remote_system_prompt.clone()),
2996
)) as Arc<dyn InferenceBackend>;
2997
(backend, label)
2998
}
3380
);
3381
}
3382
3383
+ #[test]
3384
+ #[cfg(unix)]
3385
+ fn tui_system_prompt_names_the_real_working_directory() {
3386
+ // The /init flow relies on the model knowing where the project lives:
3387
+ // without the cwd guidance it invents placeholder paths like
3388
+ // `/path/to/repo/AGENTS.md` and misreads the failure as a filesystem
3389
+ // permissions problem (issue #40).
3390
+ let _guard = ENV_TEST_LOCK
3391
+ .lock()
3392
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
3393
+ let cwd = std::env::current_dir().unwrap();
3394
+ let prompt = tui_system_prompt(true);
3395
+ assert!(prompt.starts_with(SYSTEM_PROMPT));
3396
+ assert!(prompt.contains("project working directory"));
3397
+ assert!(prompt.contains(&cwd.display().to_string()));
3398
+ }
3399
+
3400
#[test]
3401
fn parse_slash_maps_init() {
3402
assert!(matches!(parse_slash("/init"), Some(SlashCommand::Init)));