diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 5e684bfa..f10a2927 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -382,8 +382,8 @@ pub fn system_prompt_for_mode_with_context_skills_session_and_approval( // 3. Skills block. #432: walks every candidate workspace // skills directory (`.agents/skills`, `skills`, - // `.opencode/skills`, `.claude/skills`) plus the global - // default so skills installed for any AI-tool convention show + // `.opencode/skills`, `.claude/skills`, `.cursor/skills`) plus the + // global default so skills installed for any AI-tool convention show // up in the catalogue. The legacy single-`skills_dir` path is // honoured as a fallback for callers that don't supply a // workspace-aware view; it falls through to the same merged diff --git a/crates/tui/src/skills/mod.rs b/crates/tui/src/skills/mod.rs index 35dca2a5..6363d8f4 100644 --- a/crates/tui/src/skills/mod.rs +++ b/crates/tui/src/skills/mod.rs @@ -300,7 +300,8 @@ pub fn resolve_skills_dir(workspace: &Path) -> PathBuf { /// 2. `/skills` — flat, project-local. /// 3. `/.opencode/skills` — OpenCode interop. /// 4. `/.claude/skills` — Claude Code interop. -/// 5. [`default_skills_dir`] — global, user-installed. +/// 5. `/.cursor/skills` — Cursor interop. +/// 6. [`default_skills_dir`] — global, user-installed. /// /// Only directories that exist on disk are returned — callers don't /// need to filter further. Returns an empty vec when nothing is @@ -312,6 +313,7 @@ pub fn skills_directories(workspace: &Path) -> Vec { workspace.join("skills"), workspace.join(".opencode").join("skills"), workspace.join(".claude").join("skills"), + workspace.join(".cursor").join("skills"), default_skills_dir(), ]; let mut out = Vec::new(); @@ -665,10 +667,11 @@ mod tests { let tmpdir = TempDir::new().unwrap(); let workspace = tmpdir.path(); - // Create three of the four candidate dirs (skip `.opencode`). + // Create four of the five workspace candidate dirs (skip `.opencode`). std::fs::create_dir_all(workspace.join(".agents").join("skills")).unwrap(); std::fs::create_dir_all(workspace.join("skills")).unwrap(); std::fs::create_dir_all(workspace.join(".claude").join("skills")).unwrap(); + std::fs::create_dir_all(workspace.join(".cursor").join("skills")).unwrap(); let dirs = super::skills_directories(workspace); // We don't assert on the global default position because it's @@ -677,6 +680,7 @@ mod tests { let agents = workspace.join(".agents").join("skills"); let local = workspace.join("skills"); let claude = workspace.join(".claude").join("skills"); + let cursor = workspace.join(".cursor").join("skills"); assert_eq!(dirs.get(idx), Some(&agents), "agents must come first"); idx += 1; @@ -690,6 +694,12 @@ mod tests { "missing dir must be omitted, got: {dirs:?}" ); assert_eq!(dirs.get(idx), Some(&claude), "claude must come after local"); + idx += 1; + assert_eq!( + dirs.get(idx), + Some(&cursor), + "cursor must come after claude" + ); } #[test] @@ -757,6 +767,24 @@ mod tests { ); } + #[test] + fn discover_in_workspace_pulls_skills_from_cursor_dir() { + let tmpdir = TempDir::new().unwrap(); + let workspace = tmpdir.path(); + write_skill( + &workspace.join(".cursor").join("skills"), + "cursor-only", + "for cursor interop", + "body", + ); + + let registry = super::discover_in_workspace(workspace); + assert!( + registry.get("cursor-only").is_some(), + ".cursor/skills must be scanned" + ); + } + #[test] fn render_available_skills_context_for_workspace_picks_up_cross_tool_dirs() { let tmpdir = TempDir::new().unwrap(); diff --git a/crates/tui/src/tools/skill.rs b/crates/tui/src/tools/skill.rs index 0ab4ad08..6633e4a0 100644 --- a/crates/tui/src/tools/skill.rs +++ b/crates/tui/src/tools/skill.rs @@ -86,7 +86,8 @@ impl ToolSpec for LoadSkillTool { // #432: walk every candidate skill directory (workspace // .agents/skills, skills, .opencode/skills, .claude/skills, - // global default), merging with first-wins precedence. The + // .cursor/skills, global default), merging with first-wins + // precedence. The // tool's lookup mirrors what the system-prompt skills block // already lists, so the model never asks for a name it // can't find.