From 7e289568a2c26153c2db28e540fad875da068fce Mon Sep 17 00:00:00 2001 From: sanbo Date: Sun, 10 May 2026 11:27:34 +0800 Subject: [PATCH] Keep workspace skills visible when the prompt budget truncates The skills prompt renderer was re-sorting every discovered skill by name, which discarded workspace/source precedence at the last mile. Under a large global skills set, higher-priority workspace skills from directories such as `.claude/skills` could be pushed past the prompt budget and disappear from the model-visible skills list even though discovery had found them correctly. This keeps stable ordering in discovery and preserves registry order during rendering, then adds a regression test that proves a workspace-priority skill survives when lower-priority global skills overflow the prompt budget. Constraint: Session-time skill rendering must preserve cross-tool/workspace precedence Rejected: Raise the prompt budget cap | would hide the ordering bug and bloat prompts Rejected: Special-case `.claude/skills` during rendering | precedence belongs to registry order, not path-specific branches Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not re-sort rendered skills without re-proving precedence behavior under prompt truncation Tested: cargo test --all-features; cargo fmt --all -- --check; cargo clippy --all-targets --all-features Not-tested: Manual TUI interaction beyond automated skills prompt and QA PTY coverage --- crates/tui/src/skills/mod.rs | 50 +++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/tui/src/skills/mod.rs b/crates/tui/src/skills/mod.rs index 2cc52725..30bbb77a 100644 --- a/crates/tui/src/skills/mod.rs +++ b/crates/tui/src/skills/mod.rs @@ -113,6 +113,9 @@ impl SkillRegistry { let mut visited = HashSet::new(); Self::discover_recursive(dir, 0, &mut registry, &mut visited); registry + .skills + .sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.path.cmp(&b.path))); + registry } fn discover_recursive( @@ -496,9 +499,6 @@ fn render_skills_block(registry: &SkillRegistry) -> Option { return None; } - let mut skills = registry.list().to_vec(); - skills.sort_by(|a, b| a.name.cmp(&b.name)); - let mut out = String::new(); out.push_str("## Skills\n"); out.push_str( @@ -510,7 +510,7 @@ instructions when using a specific skill.\n\n", out.push_str("### Available skills\n"); let mut omitted = 0usize; - for skill in skills { + for skill in registry.list() { // Use the real on-disk path captured at discovery — the directory // name can differ from the frontmatter `name` for community // installs, in which case `//SKILL.md` would not exist @@ -770,6 +770,48 @@ mod tests { ); } + #[test] + fn render_skills_block_preserves_registry_precedence_under_prompt_budget() { + let tmpdir = TempDir::new().unwrap(); + let mut registry = super::SkillRegistry::default(); + registry.skills.push(super::Skill { + name: "workspace-priority".to_string(), + description: "must survive truncation".to_string(), + body: "body".to_string(), + path: tmpdir + .path() + .join(".claude") + .join("skills") + .join("workspace-priority") + .join("SKILL.md"), + }); + + let big_desc = "y".repeat(super::MAX_SKILL_DESCRIPTION_CHARS - 20); + for i in 0..200 { + registry.skills.push(super::Skill { + name: format!("aaa-global-{i:03}"), + description: big_desc.clone(), + body: "body".to_string(), + path: tmpdir + .path() + .join(".deepseek") + .join("skills") + .join(format!("aaa-global-{i:03}")) + .join("SKILL.md"), + }); + } + + let rendered = super::render_skills_block(®istry).expect("skill context"); + assert!( + rendered.contains("workspace-priority"), + "higher-precedence workspace skills must not be reordered behind globals:\n{rendered}" + ); + assert!( + rendered.contains("additional skills omitted from this prompt budget"), + "fixture should exceed prompt budget" + ); + } + fn write_skill(dir: &std::path::Path, name: &str, description: &str, body: &str) { let skill_dir = dir.join(name); std::fs::create_dir_all(&skill_dir).unwrap();