diff --git a/CHANGELOG.md b/CHANGELOG.md index 74881cf3..23a80751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `kimi-k2.7-code`, recognizes `kimi`/`kimi-k2` aliases for that model, keeps explicit `kimi-k2.6` selectable, and adds the OpenRouter `moonshotai/kimi-k2.7-code` registry row. +- **Ephemeral generated project context (#3058).** Opening CodeWhale in a + directory with no instruction files now keeps the bounded generated project + overview in memory instead of creating `.codewhale/instructions.md`. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 7e7d678f..1b47fea7 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `kimi-k2.7-code`, recognizes `kimi`/`kimi-k2` aliases for that model, keeps explicit `kimi-k2.6` selectable, and adds the OpenRouter `moonshotai/kimi-k2.7-code` registry row. +- **Ephemeral generated project context (#3058).** Opening CodeWhale in a + directory with no instruction files now keeps the bounded generated project + overview in memory instead of creating `.codewhale/instructions.md`. - **Cursor-style activity metadata rows (#3146).** Dense successful tool-run summaries now render as a single muted `Explored ...` / `Updated metadata` row, include short command-family labels for successful generic verifier diff --git a/crates/tui/src/project_context.rs b/crates/tui/src/project_context.rs index 54c8300f..055dca82 100644 --- a/crates/tui/src/project_context.rs +++ b/crates/tui/src/project_context.rs @@ -740,22 +740,14 @@ fn load_project_context_with_parents_and_home( } } - // Auto-generate .codewhale/instructions.md when no context file exists anywhere. - // This avoids the per-turn filesystem scan fallback in prompts.rs that - // breaks KV prefix cache stability. + // Generate a bounded in-memory fallback when no context file exists + // anywhere. This keeps prompt shape stable without creating project-local + // `.codewhale/` files merely because CodeWhale was opened in a directory. if !ctx.has_instructions() - && let Some(generated) = auto_generate_context(workspace) + && let Some(generated) = generate_ephemeral_context(workspace) { - let mut warnings = std::mem::take(&mut ctx.warnings); - ctx = load_project_context(workspace); - warnings.extend(ctx.warnings.iter().cloned()); - ctx.warnings = warnings; - if !ctx.has_instructions() { - // Loaded from the file we just wrote — use the generated content - // directly as a last resort (shouldn't normally happen). - ctx.instructions = Some(generated); - ctx.source_path = None; - } + ctx.instructions = Some(generated); + ctx.source_path = None; } // Load the CodeWhale-specific repo authority policy @@ -920,44 +912,17 @@ fn load_global_agents_context(workspace: &Path, home_dir: Option<&Path>) -> Opti None } -/// Generate a context file from project tree + summary and write it to -/// `.codewhale/instructions.md` (or `.deepseek/instructions.md` as legacy -/// fallback). Returns the generated content on success. -fn auto_generate_context(workspace: &Path) -> Option { - let codewhale_dir = workspace.join(".codewhale"); - let instructions_path = codewhale_dir.join("instructions.md"); - let legacy_instructions_path = workspace.join(".deepseek/instructions.md"); - - // Don't overwrite an existing file (check both locations) - if instructions_path.exists() || legacy_instructions_path.exists() { - return None; - } - +/// Generate ephemeral context from the project tree. Returns the generated +/// content on success without writing workspace files. +fn generate_ephemeral_context(workspace: &Path) -> Option { let overview = generate_bounded_project_overview(workspace)?; - let content = format!( - "# Project Context (Auto-generated)\n\n\ - > This file was automatically generated by CodeWhale.\n\ - > You can edit or delete it at any time.\n\n\ + Some(format!( + "# Project Context (Auto-generated, ephemeral)\n\n\ + > This context was generated in memory by CodeWhale.\n\ + > No .codewhale/instructions.md file was written.\n\n\ {overview}" - ); - - // Create .codewhale/ directory - if let Err(e) = std::fs::create_dir_all(&codewhale_dir) { - tracing::warn!("Failed to create .codewhale/ directory: {e}"); - return None; - } - - match std::fs::write(&instructions_path, &content) { - Ok(()) => { - tracing::info!("Auto-generated {}", instructions_path.display()); - Some(content) - } - Err(e) => { - tracing::warn!("Failed to write {}: {e}", instructions_path.display()); - None - } - } + )) } /// Load a context file with size checking @@ -1488,7 +1453,7 @@ mod tests { } #[test] - fn auto_generated_context_is_bounded_for_many_file_workspace() { + fn generated_context_is_bounded_and_ephemeral_for_many_file_workspace() { let workspace = tempdir().expect("workspace tempdir"); let home = tempdir().expect("home tempdir"); let noisy = workspace.path().join("aaa-many-files"); @@ -1513,9 +1478,17 @@ mod tests { assert!(ctx.has_instructions()); let generated_path = workspace.path().join(".codewhale").join("instructions.md"); - assert_eq!(ctx.source_path.as_deref(), Some(generated_path.as_path())); - let generated = fs::read_to_string(&generated_path).expect("read generated"); - assert!(generated.contains("Project Context (Auto-generated)")); + assert_eq!(ctx.source_path, None); + assert!( + !generated_path.exists(), + "generated project context should stay ephemeral" + ); + assert!( + !workspace.path().join(".codewhale").exists(), + "loading context should not create a .codewhale directory" + ); + let generated = ctx.instructions.as_ref().expect("generated instructions"); + assert!(generated.contains("Project Context (Auto-generated, ephemeral)")); assert!(generated.contains("Bounded Project Overview")); assert!(!generated.contains("")); assert!( @@ -1613,7 +1586,7 @@ mod tests { } #[test] - fn cached_context_regenerates_after_auto_generated_context_is_deleted() { + fn cached_generated_context_stays_ephemeral() { crate::project_context_cache::clear(); let workspace = tempdir().expect("workspace tempdir"); let home = tempdir().expect("home tempdir"); @@ -1622,17 +1595,17 @@ mod tests { load_project_context_with_parents_cached_and_home(workspace.path(), Some(home.path())); assert!(first.has_instructions()); let generated_path = workspace.path().join(".codewhale").join("instructions.md"); - assert!(generated_path.is_file(), "expected generated instructions"); - - fs::remove_file(&generated_path).expect("remove generated instructions"); - assert!(!generated_path.exists()); + assert!( + !generated_path.exists(), + "first load should not write generated instructions" + ); let second = load_project_context_with_parents_cached_and_home(workspace.path(), Some(home.path())); assert!(second.has_instructions()); assert!( - generated_path.is_file(), - "cache hit under the missing-file signature would skip regeneration" + !generated_path.exists(), + "cached generated context should remain in memory-only state" ); } @@ -1937,7 +1910,7 @@ mod tests { ctx.instructions .as_ref() .unwrap() - .contains("Project Context (Auto-generated)") + .contains("Project Context (Auto-generated, ephemeral)") ); } } diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 564eb34a..a1a69f2b 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -1088,9 +1088,9 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval( }; // 1–2. Mode prompt + project context. - // `load_project_context_with_parents` auto-generates .codewhale/instructions.md - // (or .deepseek/instructions.md as fallback) when no context file exists, - // so the fallback should always be available. + // `load_project_context_with_parents` generates an in-memory bounded + // overview when no context file exists, so the fallback should usually be + // available without writing project-local files. let mut full_prompt = if let Some(project_block) = project_context.as_system_block() { format!("{mode_prompt}\n\n{project_block}") } else {