mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
feat(tui): /init writes AGENTS.md in the interface language
Refs #6964. @Asteria1028 asked for `codewhale init` to generate AGENTS.md
in the language Codewhale is using. The /init prompt said "Write in
English." unconditionally. That contradicted the zh-Hans/ja/pt-BR/vi
system-prompt language bookends, and when /translate is on, it also
contradicted the translation requirement. So a Chinese-UI session got an
English file.
What
- CommandPresentationContext (command-contract facets.rs) gains
language_name(), with a default of "English". Test stubs and other hosts
need no change.
- The TUI's PresentationAdapter returns ui_locale.translation_target_name().
This is the same canonical model-facing name the translation seam already
uses, e.g. "Simplified Chinese (简体中文)".
- /init declares WORKSPACE | PRESENTATION and destructures both, each with
a safe missing-facet error. It passes the name to build_init_prompt,
whose style rule now reads "Write the whole file in {language},
headings included; keep commands, file paths and code identifiers as
they are." The doc comments that said /init consumes only WORKSPACE are
corrected.
- CHANGELOG [Unreleased] bullet, mirrored to crates/tui by
sync-changelog.sh.
- Not changed: the non-interactive `codewhale init` writes a static English
template (project_context::create_default_agents_md). Localizing it would
mean translating the template into 15 packs. The CHANGELOG says so.
Verification (source: 16e2e422e9 plus this diff)
- Real TUI in an isolated tmux socket (env -i, scratch HOME, local stub
provider), typing /init, then reading the request body the stub logged:
- LC_ALL=zh_CN.UTF-8: "Write the whole file in Simplified Chinese
(简体中文), headings included; ...", and the UI chrome rendered in
Chinese ("输入消息…", "思考:max").
- en_US.UTF-8: "Write the whole file in English, ...".
- ja_JP.UTF-8: "Write the whole file in Japanese (日本語), ...".
- scripts/dev-cargo.sh clippy --workspace --all-targets --all-features
--locked -- -D warnings -A clippy::uninlined_format_args
-A clippy::too_many_arguments -A clippy::unnecessary_map_or: clean.
- scripts/dev-test.sh tui commands::groups::project::init commands::contract
commands::groups::config: 159 tests run: 159 passed.
codewhale-portable-config + codewhale-command-contract: 114 passed.
- cargo fmt --check: clean. sync-changelog --check: up to date.
check-versions --require-dated-release: exit 0.
- npm test: root 101/101, runtime-sdk 19/19, extension-host 420 (413 pass,
7 skipped), web node 7/7, vitest 772/772. npm run check:web: exit 0.
Not verified: that a real model writes a good Chinese AGENTS.md. The stub
only proves the instruction. Windows was not run.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
16e2e422e9
commit
8867749f54
5 files changed
+55
-20
No files matched your search
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- `/init` now asks the model to write `AGENTS.md` in the interface language
|
||||
(for example Simplified Chinese when the TUI runs in zh-Hans), headings
|
||||
included, keeping commands, file paths and code identifiers as they are.
|
||||
Before, the prompt always said "Write in English". The non-interactive
|
||||
`codewhale init` template is unchanged and still English
|
||||
([#6964](https://github.com/codewhale-hq/Codewhale/issues/6964)).
|
||||
- A shell command, shell input or verifier that contains an invisible or
|
||||
bidirectional Unicode character is now refused before any approval card or
|
||||
reviewer call in Ask, Auto-Review and Never. This covers zero-width spaces
|
||||
|
||||
@@ -128,6 +128,10 @@ pub trait CommandWorkspaceContext {
|
||||
pub trait CommandPresentationContext {
|
||||
/// Resolve a stable message key with its named replacements.
|
||||
fn translate(&self, key: &str, replacements: &[(&str, &str)]) -> Result<String, String>;
|
||||
|
||||
fn language_name(&self) -> &'static str {
|
||||
"English"
|
||||
}
|
||||
}
|
||||
|
||||
/// Portable receipt for a successful atomic media attachment (FEAT-018 D4).
|
||||
|
||||
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- `/init` now asks the model to write `AGENTS.md` in the interface language
|
||||
(for example Simplified Chinese when the TUI runs in zh-Hans), headings
|
||||
included, keeping commands, file paths and code identifiers as they are.
|
||||
Before, the prompt always said "Write in English". The non-interactive
|
||||
`codewhale init` template is unchanged and still English
|
||||
([#6964](https://github.com/codewhale-hq/Codewhale/issues/6964)).
|
||||
- A shell command, shell input or verifier that contains an invisible or
|
||||
bidirectional Unicode character is now refused before any approval card or
|
||||
reviewer call in Ask, Auto-Review and Never. This covers zero-width spaces
|
||||
|
||||
@@ -2188,6 +2188,10 @@ impl CommandPresentationContext for PresentationAdapter<'_> {
|
||||
apply_named_replacements(&template, replacements)
|
||||
.ok_or_else(|| "invalid translation replacement contract".to_string())
|
||||
}
|
||||
|
||||
fn language_name(&self) -> &'static str {
|
||||
self.host.app.borrow().ui_locale.translation_target_name()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve session-control and structural-copy runtime keys to the current
|
||||
|
||||
@@ -21,10 +21,11 @@ use crate::commands::CommandResult;
|
||||
/// delegating content generation to the LLM agent.
|
||||
///
|
||||
/// FEAT-021 portable dispatch: consumes only the workspace path (supplied
|
||||
/// through the `WORKSPACE` facet); the workspace scan and prompt composition
|
||||
/// stay handler-owned (D2). `crate::utils`/`crate::project_context` are leaf,
|
||||
/// through the `WORKSPACE` facet) and the UI language name (through the
|
||||
/// `PRESENTATION` facet); the workspace scan and prompt composition stay
|
||||
/// handler-owned (D2). `crate::utils`/`crate::project_context` are leaf,
|
||||
/// path-parameterized helpers (no App/service state), consistent with D8.
|
||||
fn init(workspace: &Path) -> CommandResult {
|
||||
fn init(workspace: &Path, language: &str) -> CommandResult {
|
||||
// Ensure .deepseek/ is gitignored if we're inside a git repo.
|
||||
ensure_deepseek_gitignored(workspace);
|
||||
|
||||
@@ -43,7 +44,12 @@ fn init(workspace: &Path) -> CommandResult {
|
||||
};
|
||||
|
||||
// Construct the prompt for the LLM agent.
|
||||
let prompt = build_init_prompt(&context, existing_content.as_deref(), already_exists);
|
||||
let prompt = build_init_prompt(
|
||||
&context,
|
||||
existing_content.as_deref(),
|
||||
already_exists,
|
||||
language,
|
||||
);
|
||||
|
||||
// Display message to user AND send the prompt to the agent.
|
||||
let verb = if already_exists {
|
||||
@@ -717,6 +723,7 @@ fn build_init_prompt(
|
||||
context: &str,
|
||||
existing_content: Option<&str>,
|
||||
already_exists: bool,
|
||||
language: &str,
|
||||
) -> String {
|
||||
let mut prompt = String::new();
|
||||
|
||||
@@ -746,7 +753,7 @@ fn build_init_prompt(
|
||||
|
||||
prompt.push_str("## Instructions\n\n");
|
||||
|
||||
prompt.push_str(
|
||||
prompt.push_str(&format!(
|
||||
"1. **Read key source files** to understand the architecture:\n\
|
||||
- Start with the main entry point(s) (e.g., main.rs, index.ts, app.py)\n\
|
||||
- Read the top-level module structure to understand component boundaries\n\
|
||||
@@ -781,12 +788,13 @@ fn build_init_prompt(
|
||||
- Be concise and actionable. This is a reference document, not a tutorial.\n\
|
||||
- Use markdown headings, code blocks, and bullet lists.\n\
|
||||
- Keep the total under ~150 lines unless the project genuinely needs more.\n\
|
||||
- Write in English.\n\
|
||||
- Write the whole file in {language}, headings included; keep commands, \
|
||||
file paths and code identifiers as they are.\n\
|
||||
- Do NOT include placeholder HTML comments like \"<!-- add stuff here -->\".\n\
|
||||
- If you cannot determine something with confidence, omit that section rather than guessing.\n\n\
|
||||
4. **Write the file** using the file write tool. \
|
||||
The file should be named `AGENTS.md` at the workspace root.\n\n",
|
||||
);
|
||||
));
|
||||
|
||||
if already_exists {
|
||||
prompt.push_str(
|
||||
@@ -822,7 +830,8 @@ impl codewhale_command_contract::metadata::RegisterCommand<crate::commands::Comm
|
||||
fn handler()
|
||||
-> codewhale_command_contract::handler::CommandHandler<crate::commands::CommandResult> {
|
||||
codewhale_command_contract::handler::CommandHandler::Contextual {
|
||||
capabilities: codewhale_command_contract::handler::CommandCapabilities::WORKSPACE,
|
||||
capabilities: codewhale_command_contract::handler::CommandCapabilities::WORKSPACE
|
||||
.union(codewhale_command_contract::handler::CommandCapabilities::PRESENTATION),
|
||||
handler: init_contextual,
|
||||
}
|
||||
}
|
||||
@@ -830,10 +839,11 @@ impl codewhale_command_contract::metadata::RegisterCommand<crate::commands::Comm
|
||||
|
||||
/// Contextual `/init` dispatch (FEAT-021 Phase 4).
|
||||
///
|
||||
/// Destructures the declared `WORKSPACE` facet with a safe missing-facet
|
||||
/// error. The workspace path is consumed via the `WORKSPACE` facet (D2);
|
||||
/// Destructures the declared `WORKSPACE` and `PRESENTATION` facets with safe
|
||||
/// missing-facet errors. The workspace path is consumed via the `WORKSPACE`
|
||||
/// facet (D2) and the language the file is written in via `PRESENTATION`;
|
||||
/// `/init` consumes no project-facet method, so it destructures exactly
|
||||
/// `WORKSPACE` (D4, least-capability — the initial PROJECT|WORKSPACE
|
||||
/// those two (D4, least-capability — the initial PROJECT|WORKSPACE
|
||||
/// declaration was amended after the critical audit; main's model expresses
|
||||
/// the declaration as exact facet destructuring rather than a bitmask).
|
||||
fn init_contextual(contexts: CommandContexts<'_>, _arg: Option<&str>) -> CommandResult {
|
||||
@@ -841,7 +851,12 @@ fn init_contextual(contexts: CommandContexts<'_>, _arg: Option<&str>) -> Command
|
||||
let Some(workspace) = parts.workspace.as_deref() else {
|
||||
return crate::commands::CommandResult::error("Command capability unavailable: workspace");
|
||||
};
|
||||
init(&workspace.workspace())
|
||||
let Some(presentation) = parts.presentation.as_deref() else {
|
||||
return crate::commands::CommandResult::error(
|
||||
"Command capability unavailable: presentation",
|
||||
);
|
||||
};
|
||||
init(&workspace.workspace(), presentation.language_name())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -858,7 +873,7 @@ mod tests {
|
||||
#[test]
|
||||
fn init_returns_send_message_action() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let result = init(tmpdir.path());
|
||||
let result = init(tmpdir.path(), "English");
|
||||
assert!(result.message.is_some());
|
||||
let msg = result.message.unwrap();
|
||||
assert!(msg.contains("Creating AGENTS.md"));
|
||||
@@ -872,7 +887,7 @@ mod tests {
|
||||
fn init_says_updating_when_agents_md_exists() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
std::fs::write(tmpdir.path().join("AGENTS.md"), "existing content").unwrap();
|
||||
let result = init(tmpdir.path());
|
||||
let result = init(tmpdir.path(), "English");
|
||||
assert!(result.message.unwrap().contains("Updating AGENTS.md"));
|
||||
assert!(matches!(result.action, Some(AppAction::SendMessage(_))));
|
||||
}
|
||||
@@ -881,7 +896,7 @@ mod tests {
|
||||
fn init_includes_gitignore_handling() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
std::fs::create_dir_all(tmpdir.path().join(".git")).unwrap();
|
||||
let result = init(tmpdir.path());
|
||||
let result = init(tmpdir.path(), "English");
|
||||
assert!(!result.is_error);
|
||||
// Should have added .deepseek/ to .gitignore.
|
||||
let gi = std::fs::read_to_string(tmpdir.path().join(".gitignore")).unwrap();
|
||||
@@ -896,7 +911,7 @@ mod tests {
|
||||
"[package]\nname = \"test-crate\"\nversion = \"0.1.0\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let result = init(tmpdir.path());
|
||||
let result = init(tmpdir.path(), "English");
|
||||
let Some(AppAction::SendMessage(prompt)) = result.action else {
|
||||
panic!("expected SendMessage action");
|
||||
};
|
||||
@@ -922,7 +937,7 @@ mod tests {
|
||||
"# My Project\n\nCustom instructions here.",
|
||||
)
|
||||
.unwrap();
|
||||
let result = init(tmpdir.path());
|
||||
let result = init(tmpdir.path(), "English");
|
||||
let Some(AppAction::SendMessage(prompt)) = result.action else {
|
||||
panic!("expected SendMessage action");
|
||||
};
|
||||
@@ -1305,7 +1320,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_init_prompt_contains_all_sections() {
|
||||
let ctx = "## Project Summary\n\nA Rust project\n";
|
||||
let prompt = build_init_prompt(ctx, None, false);
|
||||
let prompt = build_init_prompt(ctx, None, false, "English");
|
||||
assert!(prompt.contains("Project Context"));
|
||||
assert!(prompt.contains("A Rust project"));
|
||||
assert!(prompt.contains("Read key source files"));
|
||||
@@ -1318,7 +1333,7 @@ mod tests {
|
||||
fn build_init_prompt_with_existing_content() {
|
||||
let ctx = "## Project Summary\n\nA Rust project\n";
|
||||
let existing = "# Old AGENTS.md content";
|
||||
let prompt = build_init_prompt(ctx, Some(existing), true);
|
||||
let prompt = build_init_prompt(ctx, Some(existing), true, "English");
|
||||
assert!(prompt.contains("Old AGENTS.md content"));
|
||||
assert!(prompt.contains("Update it in place"));
|
||||
}
|
||||
@@ -1326,7 +1341,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_init_prompt_new_file_no_update_instruction() {
|
||||
let ctx = "## Project Summary\n\nA Rust project\n";
|
||||
let prompt = build_init_prompt(ctx, None, false);
|
||||
let prompt = build_init_prompt(ctx, None, false, "English");
|
||||
assert!(!prompt.contains("The file already exists"));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user