99
#[cfg(unix)]
100
use std::os::unix::io::{AsRawFd, FromRawFd};
101
102
+/// Stands in for the commit trailer inside [`SYSTEM_PROMPT`]; see
103
+/// [`system_prompt_for_model`].
104
+const CO_AUTHOR_TRAILER_PLACEHOLDER: &str = "{commit_co_author_trailer}";
105
+
106
const SYSTEM_PROMPT: &str = concat!(
107
"\
108
Your name is siGit — lowercase 's', uppercase 'G', no spaces. \
161
directory, permissions), and retry
162
- when you create a commit, always end the commit message with a blank line and \
163
then this trailer on its own line: ",
160
- // Same text as tools::COMMIT_CO_AUTHOR_TRAILER; concat! takes only
161
- // literals, so it is spelled out here and a test keeps the two in step.
162
- "Co-Authored-By: siGit Code v",
163
- env!("CARGO_PKG_VERSION"),
164
- " <noreply@sigit.si> \
164
+ // CO_AUTHOR_TRAILER_PLACEHOLDER (concat! takes only literals), filled in
165
+ // by `system_prompt_for_model` with tools::commit_co_author_trailer(),
166
+ // which depends on the surface picked at startup.
167
+ "{commit_co_author_trailer}",
168
+ " \
169
— GitHub reads that exact format and credits siGit as co-author. If a commit \
170
lands without it, siGit Code amends the trailer in automatically and the tool \
171
output says so; do not amend again yourself.
250
When debugging, address the root cause, not the symptom. \
251
Be direct and brief.";
252
249
-pub(crate) fn system_prompt_for_model(tool_calling: bool) -> &'static str {
253
+pub(crate) fn system_prompt_for_model(tool_calling: bool) -> String {
254
if tool_calling {
251
- SYSTEM_PROMPT
255
+ SYSTEM_PROMPT.replace(
256
+ CO_AUTHOR_TRAILER_PLACEHOLDER,
257
+ &tools::commit_co_author_trailer(),
258
+ )
259
} else {
253
- SIMPLE_SYSTEM_PROMPT
260
+ SIMPLE_SYSTEM_PROMPT.to_string()
261
}
262
}
263
909
};
910
911
let loader_engine = Arc::clone(&self.engine);
905
- let loader_system_prompt = system_prompt_for_model(tool_calling).to_string();
912
+ let loader_system_prompt = system_prompt_for_model(tool_calling);
913
let model_ready = Arc::clone(&self.model_ready);
914
let model_load_error = Arc::clone(&self.model_load_error);
915
1340
let (result_tx, result_rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
1341
let loader_engine = Arc::clone(&self.engine);
1342
let loader_config = new_config.clone();
1336
- let loader_system_prompt = system_prompt_for_model(new_tool_calling).to_string();
1343
+ let loader_system_prompt = system_prompt_for_model(new_tool_calling);
1344
let loader_sampling = sampling;
1345
1346
std::thread::spawn(move || {
2660
&cwd,
2661
&workspace::additional_roots(),
2662
)),
2656
- None => system_prompt_for_model(true).to_string(),
2663
+ None => system_prompt_for_model(true),
2664
};
2665
let cloud_backend: Arc<dyn InferenceBackend> = Arc::new(OpenAiBackend::new(
2666
cfg.base_url,
4109
provider.base_url,
4110
provider.api_key,
4111
provider.model,
4105
- Some(with_instructions(SYSTEM_PROMPT.to_string())),
4112
+ Some(with_instructions(system_prompt_for_model(true))),
4113
)) as Arc<dyn InferenceBackend>;
4114
(backend, label)
4115
}
4139
provider.base_url,
4140
provider.api_key,
4141
provider.model,
4135
- Some(with_instructions(SYSTEM_PROMPT.to_string())),
4142
+ Some(with_instructions(system_prompt_for_model(true))),
4143
)) as Arc<dyn InferenceBackend>;
4144
(backend, label)
4145
}
4292
cfg.base_url,
4293
cfg.api_key,
4294
cfg.model,
4288
- Some(system_prompt_for_model(true).to_string()),
4295
+ Some(system_prompt_for_model(true)),
4296
));
4297
*state.backend.lock().await = override_backend;
4298
} else {
4552
std::process::exit(2);
4553
}
4554
Ok(true) => {
4555
+ tools::set_surface(tools::Surface::Acp);
4556
init_logging(false);
4557
setup::setup_shared_model_cache();
4558
mcp::init().await;
4567
match headless::parse_args(&cli_args) {
4568
Ok(None) => {}
4569
Ok(Some(config)) => {
4570
+ tools::set_surface(tools::Surface::Headless);
4571
// Logs to stderr; stdout stays clean for the assistant's answer.
4572
init_logging(false);
4573
// Enter --cwd before anything loads, so instruction files and
4616
// must redirect before any library code touches stdout
4617
#[cfg(unix)]
4618
{
4619
+ tools::set_surface(tools::Surface::Tui);
4620
let (tty, cleanup_tty) = redirect_output_to_log()?;
4621
init_logging(true);
4622
setup::setup_shared_model_cache();
4632
} else {
4633
// ACP mode: keep stdout untouched for protocol JSON only.
4634
// Logs already go to stderr via `init_logging(false)`.
4635
+ tools::set_surface(tools::Surface::Acp);
4636
init_logging(false);
4637
setup::setup_shared_model_cache();
4638
// Best-effort MCP discovery (incl. the official server) before serving.
5185
// The prompt instructs the model with the exact trailer that
5186
// `tools::ensure_commit_co_author` enforces; if the two drift apart the
5187
// safety net would re-amend commits the model already attributed.
5188
+ let prompt = system_prompt_for_model(true);
5189
assert!(
5178
- SYSTEM_PROMPT.contains(tools::COMMIT_CO_AUTHOR_TRAILER),
5179
- "SYSTEM_PROMPT must quote tools::COMMIT_CO_AUTHOR_TRAILER verbatim"
5190
+ prompt.contains(&tools::commit_co_author_trailer()),
5191
+ "the system prompt must quote tools::commit_co_author_trailer() verbatim"
5192
);
5193
+ assert!(!prompt.contains(CO_AUTHOR_TRAILER_PLACEHOLDER));
5194
}
5195
5196
#[test]