fix(lang): keep hidden reasoning_content in English regardless of locale (#1842/#1843)
- Add show_thinking flag to PromptSessionContext - When show_thinking=false, emit hidden-thinking English instruction - Omit locale-reinforcement bookends when user can't see thinking blocks - Keep final-visible-reply language rule unchanged - Add test for hidden-thinking language directive
This commit is contained in:
@@ -99,6 +99,9 @@ pub struct EngineConfig {
|
||||
/// When true, the model is instructed to respond in the current locale
|
||||
/// and a post-hoc translation layer replaces remaining English output.
|
||||
pub translation_enabled: bool,
|
||||
/// Whether user-visible transcript rendering shows thinking blocks.
|
||||
/// Prompt assembly uses this to avoid localizing hidden reasoning.
|
||||
pub show_thinking: bool,
|
||||
/// Maximum number of assistant steps before stopping.
|
||||
pub max_steps: u32,
|
||||
/// Maximum number of concurrently active subagents.
|
||||
@@ -194,6 +197,7 @@ impl Default for EngineConfig {
|
||||
instructions: Vec::new(),
|
||||
project_context_pack_enabled: true,
|
||||
translation_enabled: false,
|
||||
show_thinking: true,
|
||||
max_steps: 100,
|
||||
max_subagents: DEFAULT_MAX_SUBAGENTS,
|
||||
features: Features::with_defaults(),
|
||||
@@ -455,6 +459,7 @@ impl Engine {
|
||||
locale_tag: &config.locale_tag,
|
||||
translation_enabled: config.translation_enabled,
|
||||
model_id: &config.model,
|
||||
show_thinking: config.show_thinking,
|
||||
},
|
||||
session.approval_mode,
|
||||
);
|
||||
@@ -610,6 +615,7 @@ impl Engine {
|
||||
auto_approve,
|
||||
approval_mode,
|
||||
translation_enabled,
|
||||
show_thinking,
|
||||
} => {
|
||||
self.handle_send_message(
|
||||
content,
|
||||
@@ -624,6 +630,7 @@ impl Engine {
|
||||
auto_approve,
|
||||
approval_mode,
|
||||
translation_enabled,
|
||||
show_thinking,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -830,6 +837,7 @@ impl Engine {
|
||||
self.session.auto_approve,
|
||||
self.session.approval_mode,
|
||||
self.config.translation_enabled,
|
||||
self.config.show_thinking,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -918,6 +926,7 @@ impl Engine {
|
||||
auto_approve: bool,
|
||||
approval_mode: crate::tui::approval::ApprovalMode,
|
||||
translation_enabled: bool,
|
||||
show_thinking: bool,
|
||||
) {
|
||||
// Reset cancel token for fresh turn (in case previous was cancelled)
|
||||
self.reset_cancel_token();
|
||||
@@ -1011,6 +1020,7 @@ impl Engine {
|
||||
self.session.trust_mode = trust_mode;
|
||||
self.config.trust_mode = trust_mode;
|
||||
self.config.translation_enabled = translation_enabled;
|
||||
self.config.show_thinking = show_thinking;
|
||||
self.session.auto_approve = auto_approve;
|
||||
self.session.approval_mode = if auto_approve {
|
||||
crate::tui::approval::ApprovalMode::Auto
|
||||
@@ -1852,6 +1862,7 @@ impl Engine {
|
||||
locale_tag: &self.config.locale_tag,
|
||||
translation_enabled: self.config.translation_enabled,
|
||||
model_id: &self.config.model,
|
||||
show_thinking: self.config.show_thinking,
|
||||
},
|
||||
self.session.approval_mode,
|
||||
);
|
||||
|
||||
@@ -1509,6 +1509,28 @@ fn refresh_system_prompt_is_noop_when_unchanged() {
|
||||
assert_eq!(engine.session.system_prompt, first_prompt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_prompt_respects_hidden_thinking_config() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let config = EngineConfig {
|
||||
workspace: tmp.path().to_path_buf(),
|
||||
locale_tag: "zh-Hans".to_string(),
|
||||
show_thinking: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (engine, _handle) = Engine::new(config, &Config::default());
|
||||
let prompt = match engine.session.system_prompt.as_ref() {
|
||||
Some(SystemPrompt::Text(text)) => text,
|
||||
Some(SystemPrompt::Blocks(_)) => panic!("expected text system prompt"),
|
||||
None => panic!("expected system prompt"),
|
||||
};
|
||||
|
||||
assert!(prompt.contains("## Hidden Thinking Language"));
|
||||
assert!(prompt.contains("reasoning_content"));
|
||||
assert!(prompt.contains("English"));
|
||||
assert!(!prompt.contains("## 语言再次提醒"));
|
||||
}
|
||||
|
||||
fn sync_runtime_system_prompt_override(engine: &mut Engine, system_prompt: SystemPrompt) {
|
||||
engine.session.compaction_summary_prompt =
|
||||
extract_compaction_summary_prompt(Some(system_prompt.clone()));
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum Op {
|
||||
auto_approve: bool,
|
||||
approval_mode: ApprovalMode,
|
||||
translation_enabled: bool,
|
||||
show_thinking: bool,
|
||||
},
|
||||
|
||||
/// Cancel the current request
|
||||
|
||||
@@ -5160,6 +5160,7 @@ async fn run_exec_agent(
|
||||
.lsp
|
||||
.clone()
|
||||
.map(crate::config::LspConfigToml::into_runtime);
|
||||
let settings = crate::settings::Settings::load().unwrap_or_default();
|
||||
|
||||
let engine_config = EngineConfig {
|
||||
model: effective_model.clone(),
|
||||
@@ -5172,6 +5173,7 @@ async fn run_exec_agent(
|
||||
instructions: config.instructions_paths(),
|
||||
project_context_pack_enabled: config.project_context_pack_enabled(),
|
||||
translation_enabled: false,
|
||||
show_thinking: settings.show_thinking,
|
||||
max_steps: 100,
|
||||
max_subagents,
|
||||
features: config.features(),
|
||||
@@ -5197,11 +5199,9 @@ async fn run_exec_agent(
|
||||
vision_config: config.vision_model_config(),
|
||||
strict_tool_mode: config.strict_tool_mode.unwrap_or(false),
|
||||
goal_objective: None,
|
||||
locale_tag: crate::localization::resolve_locale(
|
||||
&crate::settings::Settings::load().unwrap_or_default().locale,
|
||||
)
|
||||
.tag()
|
||||
.to_string(),
|
||||
locale_tag: crate::localization::resolve_locale(&settings.locale)
|
||||
.tag()
|
||||
.to_string(),
|
||||
workshop: config.workshop.clone(),
|
||||
search_provider: config.search_provider(),
|
||||
search_api_key: config.search.as_ref().and_then(|s| s.api_key.clone()),
|
||||
@@ -5260,6 +5260,7 @@ async fn run_exec_agent(
|
||||
trust_mode,
|
||||
auto_approve,
|
||||
translation_enabled: false,
|
||||
show_thinking: settings.show_thinking,
|
||||
approval_mode: if auto_approve {
|
||||
crate::tui::approval::ApprovalMode::Auto
|
||||
} else {
|
||||
|
||||
+102
-2
@@ -34,6 +34,10 @@ pub struct PromptSessionContext<'a> {
|
||||
/// preserving backward compatibility with existing call sites
|
||||
/// that predate dynamic model injection.
|
||||
pub model_id: &'a str,
|
||||
/// Whether the user-visible transcript renders thinking blocks.
|
||||
/// When false, the prompt should not spend localization pressure on
|
||||
/// `reasoning_content` the user will never see.
|
||||
pub show_thinking: bool,
|
||||
}
|
||||
|
||||
impl Default for PromptSessionContext<'_> {
|
||||
@@ -45,6 +49,7 @@ impl Default for PromptSessionContext<'_> {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +109,25 @@ fn translation_target_language_for_tag(locale_tag: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn hidden_thinking_language_instruction(locale_tag: &str) -> String {
|
||||
let fallback_language = translation_target_language_for_tag(locale_tag);
|
||||
format!(
|
||||
"\
|
||||
## Hidden Thinking Language\n\
|
||||
\n\
|
||||
The user has disabled thinking display (`show_thinking = false`). If you emit \
|
||||
`reasoning_content`, keep that hidden internal thinking in English regardless \
|
||||
of the latest user-message language or `## Environment.lang`; the user will \
|
||||
not see it, so localizing hidden thinking only adds language switching.\n\
|
||||
\n\
|
||||
The final reply is still user-visible. Follow the normal `## Language` rule \
|
||||
for the final reply: mirror the latest user message, and use \
|
||||
{fallback_language} only when the user message is ambiguous. If the user \
|
||||
explicitly asks for a different thinking language, follow that explicit request \
|
||||
for the current turn."
|
||||
)
|
||||
}
|
||||
|
||||
/// Render a `## Environment` block listing the resolved locale tag,
|
||||
/// runtime version, host platform, login shell, and current working directory.
|
||||
///
|
||||
@@ -611,6 +635,7 @@ pub fn system_prompt_for_mode_with_context_and_skills(
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -657,7 +682,11 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
|
||||
// in English even though `lang: zh-Hans` is set" failure mode that
|
||||
// PR #1398 partially addressed. English (and unknown) locales get
|
||||
// `None` and keep the previous behavior unchanged.
|
||||
let preamble = locale_reinforcement_preamble(session_context.locale_tag);
|
||||
let preamble = if session_context.show_thinking {
|
||||
locale_reinforcement_preamble(session_context.locale_tag)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 1–2. Mode prompt + project context.
|
||||
// `load_project_context_with_parents` auto-generates .codewhale/instructions.md
|
||||
@@ -806,8 +835,17 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval(
|
||||
// rule immediately before it generates `reasoning_content` for the
|
||||
// turn. English (and unknown) locales return `None` and the prompt
|
||||
// stays byte-identical to the pre-bookend behavior.
|
||||
if let Some(closer) = locale_reinforcement_closer(session_context.locale_tag) {
|
||||
if let Some(closer) = session_context
|
||||
.show_thinking
|
||||
.then(|| locale_reinforcement_closer(session_context.locale_tag))
|
||||
.flatten()
|
||||
{
|
||||
full_prompt = format!("{full_prompt}\n\n{closer}");
|
||||
} else if !session_context.show_thinking {
|
||||
full_prompt = format!(
|
||||
"{full_prompt}\n\n{}",
|
||||
hidden_thinking_language_instruction(session_context.locale_tag)
|
||||
);
|
||||
}
|
||||
|
||||
SystemPrompt::Text(full_prompt)
|
||||
@@ -1087,6 +1125,7 @@ mod tests {
|
||||
locale_tag: "zh-Hans",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
ApprovalMode::Suggest,
|
||||
) {
|
||||
@@ -1157,6 +1196,7 @@ mod tests {
|
||||
locale_tag: "zh-Hans",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
ApprovalMode::Suggest,
|
||||
) {
|
||||
@@ -1184,6 +1224,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_thinking_uses_english_reasoning_without_locale_bookends() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let text = match system_prompt_for_mode_with_context_skills_session_and_approval(
|
||||
AppMode::Agent,
|
||||
tmp.path(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
PromptSessionContext {
|
||||
user_memory_block: None,
|
||||
goal_objective: None,
|
||||
project_context_pack_enabled: false,
|
||||
locale_tag: "zh-Hans",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: false,
|
||||
},
|
||||
ApprovalMode::Suggest,
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
SystemPrompt::Blocks(_) => panic!("expected text system prompt"),
|
||||
};
|
||||
|
||||
assert!(
|
||||
text.contains("## Hidden Thinking Language"),
|
||||
"hidden thinking prompt must include the request-side language override"
|
||||
);
|
||||
assert!(
|
||||
text.contains("reasoning_content") && text.contains("English"),
|
||||
"hidden thinking override must steer reasoning_content to English"
|
||||
);
|
||||
assert!(
|
||||
text.contains("final reply") && text.contains("Simplified Chinese"),
|
||||
"hidden thinking override must preserve the visible reply language"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("## 语言要求") && !text.contains("## 语言再次提醒"),
|
||||
"hidden thinking prompt must not also ask for localized reasoning"
|
||||
);
|
||||
|
||||
let hidden_pos = text
|
||||
.find("## Hidden Thinking Language")
|
||||
.expect("hidden thinking block present");
|
||||
let hidden_header_end = hidden_pos + "## Hidden Thinking Language".len();
|
||||
let after_hidden_body = &text[hidden_header_end..];
|
||||
assert!(
|
||||
!after_hidden_body.contains("\n## "),
|
||||
"hidden thinking override must be the final top-level block; got: {after_hidden_body:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_prompt_skips_locale_preamble_for_english() {
|
||||
// English locale → no preamble injected. Asserts the
|
||||
@@ -1202,6 +1294,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
ApprovalMode::Suggest,
|
||||
) {
|
||||
@@ -1295,6 +1388,7 @@ mod tests {
|
||||
locale_tag: "ja",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1331,6 +1425,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1359,6 +1454,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1416,6 +1512,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1444,6 +1541,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1639,6 +1737,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
@@ -1673,6 +1772,7 @@ mod tests {
|
||||
locale_tag: "en",
|
||||
translation_enabled: false,
|
||||
model_id: "codewhale",
|
||||
show_thinking: true,
|
||||
},
|
||||
) {
|
||||
SystemPrompt::Text(text) => text,
|
||||
|
||||
@@ -1611,6 +1611,9 @@ impl RuntimeThreadManager {
|
||||
let allow_shell = req.allow_shell.unwrap_or(thread.allow_shell);
|
||||
let trust_mode = req.trust_mode.unwrap_or(thread.trust_mode);
|
||||
let auto_approve = req.auto_approve.unwrap_or(thread.auto_approve);
|
||||
let show_thinking = crate::settings::Settings::load()
|
||||
.unwrap_or_default()
|
||||
.show_thinking;
|
||||
|
||||
engine
|
||||
.send(Op::SendMessage {
|
||||
@@ -1625,6 +1628,7 @@ impl RuntimeThreadManager {
|
||||
trust_mode,
|
||||
auto_approve,
|
||||
translation_enabled: false,
|
||||
show_thinking,
|
||||
approval_mode: if auto_approve {
|
||||
crate::tui::approval::ApprovalMode::Auto
|
||||
} else {
|
||||
@@ -1931,6 +1935,7 @@ impl RuntimeThreadManager {
|
||||
.lsp
|
||||
.clone()
|
||||
.map(crate::config::LspConfigToml::into_runtime);
|
||||
let settings = crate::settings::Settings::load().unwrap_or_default();
|
||||
let engine_cfg = EngineConfig {
|
||||
model: thread.model.clone(),
|
||||
workspace: thread.workspace.clone(),
|
||||
@@ -1942,6 +1947,7 @@ impl RuntimeThreadManager {
|
||||
instructions: self.config.instructions_paths(),
|
||||
project_context_pack_enabled: self.config.project_context_pack_enabled(),
|
||||
translation_enabled: false,
|
||||
show_thinking: settings.show_thinking,
|
||||
max_steps: 100,
|
||||
max_subagents: self.config.max_subagents().clamp(1, MAX_SUBAGENTS),
|
||||
features: self.config.features(),
|
||||
@@ -1982,11 +1988,9 @@ impl RuntimeThreadManager {
|
||||
vision_config: self.config.vision_model_config(),
|
||||
strict_tool_mode: self.config.strict_tool_mode.unwrap_or(false),
|
||||
goal_objective: None,
|
||||
locale_tag: crate::localization::resolve_locale(
|
||||
&crate::settings::Settings::load().unwrap_or_default().locale,
|
||||
)
|
||||
.tag()
|
||||
.to_string(),
|
||||
locale_tag: crate::localization::resolve_locale(&settings.locale)
|
||||
.tag()
|
||||
.to_string(),
|
||||
workshop: self.config.workshop.clone(),
|
||||
search_provider: self.config.search_provider(),
|
||||
search_api_key: self.config.search.as_ref().and_then(|s| s.api_key.clone()),
|
||||
|
||||
@@ -684,6 +684,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
|
||||
instructions: config.instructions_paths(),
|
||||
project_context_pack_enabled: config.project_context_pack_enabled(),
|
||||
translation_enabled: app.translation_enabled,
|
||||
show_thinking: app.show_thinking,
|
||||
// Effectively unlimited. V4 has a 1M context window and the user
|
||||
// wants the model running until it's actually done. The previous cap
|
||||
// of 100 hit the ceiling on long multi-step plans (wide refactors,
|
||||
@@ -4147,6 +4148,7 @@ async fn dispatch_user_message(
|
||||
locale_tag: app.ui_locale.tag(),
|
||||
translation_enabled: app.translation_enabled,
|
||||
model_id: &app.model,
|
||||
show_thinking: app.show_thinking,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -4243,6 +4245,7 @@ async fn dispatch_user_message(
|
||||
auto_approve: app.mode == AppMode::Yolo,
|
||||
approval_mode: app.approval_mode,
|
||||
translation_enabled: app.translation_enabled,
|
||||
show_thinking: app.show_thinking,
|
||||
})
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -3140,6 +3140,7 @@ async fn dismissed_plan_prompt_leaves_non_numeric_input_for_normal_send_path() {
|
||||
#[tokio::test]
|
||||
async fn dispatch_user_message_records_prompt_for_cancel_restore() {
|
||||
let mut app = create_test_app();
|
||||
app.show_thinking = false;
|
||||
let config = Config::default();
|
||||
let mut engine = crate::core::engine::mock_engine_handle();
|
||||
let queued = crate::tui::app::QueuedMessage::new("fix this typo\nthen retry".to_string(), None);
|
||||
@@ -3153,8 +3154,16 @@ async fn dispatch_user_message_records_prompt_for_cancel_restore() {
|
||||
Some("fix this typo\nthen retry")
|
||||
);
|
||||
match engine.rx_op.recv().await.expect("send message op") {
|
||||
crate::core::ops::Op::SendMessage { content, .. } => {
|
||||
crate::core::ops::Op::SendMessage {
|
||||
content,
|
||||
show_thinking,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(content, "fix this typo\nthen retry");
|
||||
assert!(
|
||||
!show_thinking,
|
||||
"dispatch must carry the user's hidden-thinking setting into the engine"
|
||||
);
|
||||
}
|
||||
other => panic!("expected SendMessage, got {other:?}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user