feat(skills): expose installed skills to the model + zh-CN README
Add a model-visible skills block to the system prompt (progressive disclosure: lists name/description/path, never inlines SKILL.md bodies) with a 12k-char prompt budget and a 512-char per-description cap. EngineConfig gains skills_dir, threaded through the three construction sites (TUI app, exec agent, runtime thread manager). README skills section is rewritten to document the discovery order, the SKILL.md frontmatter contract, and the install/update/uninstall/ trust commands. Adds Simplified Chinese README cross-link and full README.zh-CN.md translation (DeepSeek went viral in CN -- discoverability matters). Tests cover happy path, empty/missing dir → None, oversize description truncation with U+2026 marker, internal-whitespace collapse, and the overflow-budget omission notice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
> **A terminal-native coding agent for [DeepSeek V4](https://platform.deepseek.com) models — with 1M-token context, thinking-mode reasoning, and full tool-use.**
|
||||
|
||||
[简体中文 README](README.zh-CN.md)
|
||||
|
||||
```bash
|
||||
npm i -g deepseek-tui
|
||||
```
|
||||
@@ -332,8 +334,37 @@ or via the `LC_ALL`/`LANG` environment variables. See [docs/CONFIGURATION.md](do
|
||||
|
||||
## Publishing your own skill
|
||||
|
||||
DeepSeek-TUI can install community skills directly from a GitHub repo, with no
|
||||
backend service in the loop:
|
||||
DeepSeek-TUI discovers skills from the active skills directory. Workspace-local
|
||||
`.agents/skills` wins when present, then `./skills`, then the configured global
|
||||
directory (`~/.deepseek/skills` by default). Each skill is a directory with a
|
||||
`SKILL.md` file:
|
||||
|
||||
```text
|
||||
~/.deepseek/skills/my-skill/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
`SKILL.md` must start with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: Use this when DeepSeek should follow my custom workflow.
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
Instructions for the agent go here.
|
||||
```
|
||||
|
||||
Run `/skills` to list discovered skills, `/skill <name>` to activate one for
|
||||
the next message, or `/skill new` to use the bundled skill-creator helper.
|
||||
Installed skills are also listed in the model-visible session context so the
|
||||
agent can choose relevant skills when the user names them or when the task
|
||||
matches their descriptions.
|
||||
|
||||
DeepSeek-TUI can also install community skills directly from a GitHub repo,
|
||||
with no backend service in the loop:
|
||||
|
||||
1. Create a public GitHub repo with a `SKILL.md` at the root containing the
|
||||
usual `---` frontmatter (`name`, `description`).
|
||||
@@ -346,6 +377,9 @@ backend service in the loop:
|
||||
placed under `~/.deepseek/skills/<name>/`.
|
||||
5. Submit a PR to the curated `index.json` (default registry) to make the skill
|
||||
installable by name (`/skill install <name>`) instead of the GitHub spec.
|
||||
6. Use `/skill update <name>`, `/skill uninstall <name>`, or
|
||||
`/skill trust <name>` for installed community skills. Trust is only needed
|
||||
when you want scripts bundled with a skill to be eligible for execution.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
# DeepSeek TUI
|
||||
|
||||
> **面向 [DeepSeek V4](https://platform.deepseek.com) 模型的终端原生编程智能体,支持 100 万 token 上下文、思考模式推理流和完整工具调用。**
|
||||
|
||||
[English README](README.md)
|
||||
|
||||
```bash
|
||||
npm i -g deepseek-tui
|
||||
```
|
||||
|
||||
[](https://github.com/Hmbown/DeepSeek-TUI/actions/workflows/ci.yml)
|
||||
[](https://www.npmjs.com/package/deepseek-tui)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 这是什么?
|
||||
|
||||
DeepSeek TUI 是一个完全运行在终端里的编程智能体。它可以让 DeepSeek 前沿模型直接访问你的工作区:读取和编辑文件、运行 shell 命令、搜索和浏览网页、管理 git、调度子智能体,并通过快速的键盘驱动 TUI 完成多步开发任务。
|
||||
|
||||
它面向 **DeepSeek V4**(`deepseek-v4-pro` / `deepseek-v4-flash`)构建,默认支持 100 万 token 上下文窗口和原生思考模式流式输出。模型推理、工具调用和最终回答会在终端里实时呈现。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **原生 RLM**(`rlm_query` 工具):用现有 DeepSeek 客户端并行调度 1 到 16 个低成本 `deepseek-v4-flash` 子任务,用于批量分析、任务拆解或并行推理。
|
||||
- **思考模式流式输出**:实时显示 DeepSeek 的推理过程。
|
||||
- **完整工具集**:文件操作、shell 执行、git、网页搜索/浏览、apply-patch、子智能体、MCP 服务器。
|
||||
- **100 万 token 上下文**:上下文接近上限时自动进行智能压缩。
|
||||
- **三种交互模式**:Plan(只读探索)、Agent(默认交互并带审批)、YOLO(可信工作区内自动批准工具)。
|
||||
- **推理强度档位**:用 `Shift+Tab` 在 `off -> high -> max` 之间切换。
|
||||
- **会话保存和恢复**:适合长任务的断点续作。
|
||||
- **工作区回滚**:通过 side-git 记录每轮前后快照,支持 `/restore` 和 `revert_turn`,不修改项目自己的 `.git`。
|
||||
- **HTTP/SSE 运行时 API**:`deepseek serve --http` 可用于无界面智能体流程。
|
||||
- **MCP 协议支持**:连接 Model Context Protocol 服务器扩展工具,见 [docs/MCP.md](docs/MCP.md)。
|
||||
- **实时成本跟踪**:按轮次和会话统计 token 用量与成本估算。
|
||||
- **深色主题**:DeepSeek 蓝色系终端界面。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
npm install -g deepseek-tui
|
||||
deepseek
|
||||
```
|
||||
|
||||
首次启动时会提示输入 [DeepSeek API key](https://platform.deepseek.com/api_keys)。也可以提前配置:
|
||||
|
||||
```bash
|
||||
# 通过 CLI 保存
|
||||
deepseek login --api-key "YOUR_DEEPSEEK_API_KEY"
|
||||
|
||||
# 或通过环境变量
|
||||
export DEEPSEEK_API_KEY="YOUR_DEEPSEEK_API_KEY"
|
||||
deepseek
|
||||
```
|
||||
|
||||
### 中国大陆 / 镜像友好安装
|
||||
|
||||
如果在中国大陆访问 GitHub 或 npm 下载较慢,可以通过 Cargo 注册表镜像安装 Rust crate:
|
||||
|
||||
```toml
|
||||
# ~/.cargo/config.toml
|
||||
[source.crates-io]
|
||||
replace-with = "tuna"
|
||||
|
||||
[source.tuna]
|
||||
registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"
|
||||
```
|
||||
|
||||
然后从主包安装已发布的两个二进制文件:
|
||||
|
||||
```bash
|
||||
cargo install deepseek-tui --locked
|
||||
deepseek --version
|
||||
deepseek doctor --json
|
||||
```
|
||||
|
||||
从 `v0.8.1` 开始,`deepseek-tui` 这个主 Cargo 包会同时安装:
|
||||
|
||||
- `deepseek`:推荐使用的调度器入口。
|
||||
- `deepseek-tui`:交互式 TUI 伴随二进制。
|
||||
|
||||
也可以直接从 [GitHub Releases](https://github.com/Hmbown/DeepSeek-TUI/releases) 下载预编译二进制。如果你有镜像后的 release 资产目录,也可以配合 `DEEPSEEK_TUI_RELEASE_BASE_URL` 使用 TUNA、rsproxy、腾讯云 COS 或阿里云 OSS 等镜像。
|
||||
|
||||
### 从源码安装
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Hmbown/DeepSeek-TUI.git
|
||||
cd DeepSeek-TUI
|
||||
cargo install --path crates/tui --locked # 需要 Rust 1.85+
|
||||
deepseek --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 其他模型提供方
|
||||
|
||||
### NVIDIA NIM
|
||||
|
||||
```bash
|
||||
deepseek auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY"
|
||||
deepseek --provider nvidia-nim
|
||||
|
||||
# 或仅对当前进程生效:
|
||||
DEEPSEEK_PROVIDER=nvidia-nim NVIDIA_API_KEY="..." deepseek
|
||||
```
|
||||
|
||||
### Fireworks 和自托管 SGLang
|
||||
|
||||
```bash
|
||||
deepseek auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"
|
||||
deepseek --provider fireworks --model deepseek-v4-pro
|
||||
|
||||
# SGLang 通常是自托管;localhost 部署可以不配置鉴权。
|
||||
SGLANG_BASE_URL="http://localhost:30000/v1" deepseek --provider sglang --model deepseek-v4-flash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
```bash
|
||||
deepseek # 交互式 TUI
|
||||
deepseek "explain this function" # 一次性提示
|
||||
deepseek --model deepseek-v4-flash "summarize" # 指定模型
|
||||
deepseek --yolo # YOLO 模式,自动批准工具
|
||||
deepseek login --api-key "..." # 保存 API key
|
||||
deepseek doctor # 检查配置和连接
|
||||
deepseek doctor --json # 机器可读诊断
|
||||
deepseek setup --status # 只读安装状态检查
|
||||
deepseek setup --tools --plugins # 创建本地工具和插件目录
|
||||
deepseek models # 列出可用 API 模型
|
||||
deepseek sessions # 列出已保存会话
|
||||
deepseek resume --last # 恢复最近会话
|
||||
deepseek serve --http # HTTP/SSE API 服务
|
||||
deepseek mcp list # 列出已配置 MCP 服务器
|
||||
deepseek mcp validate # 校验 MCP 配置和连接
|
||||
deepseek mcp-server # 启动 dispatcher MCP stdio 服务器
|
||||
```
|
||||
|
||||
### 常用快捷键
|
||||
|
||||
| 按键 | 功能 |
|
||||
|---|---|
|
||||
| `Tab` | 补全 `/` 或 `@`;运行中则把草稿排队为后续消息;否则切换模式 |
|
||||
| `Shift+Tab` | 切换推理强度:off -> high -> max |
|
||||
| `F1` | 帮助 |
|
||||
| `Esc` | 返回 / 关闭 |
|
||||
| `Ctrl+K` | 命令面板 |
|
||||
| `Ctrl+R` | 恢复旧会话 |
|
||||
| `Alt+R` | 搜索提示历史和恢复草稿 |
|
||||
| `@path` | 在输入框中附加文件或目录上下文 |
|
||||
| `Alt+↑` | 编辑最后一条排队消息 |
|
||||
| `/attach <path>` | 附加图片或视频路径引用 |
|
||||
|
||||
---
|
||||
|
||||
## 模式
|
||||
|
||||
| 模式 | 行为 |
|
||||
|---|---|
|
||||
| **Plan** | 只读调查;模型先探索并提出拆解计划,再进行更改 |
|
||||
| **Agent** | 默认交互模式;多步工具调用带审批门禁 |
|
||||
| **YOLO** | 在可信工作区自动批准工具;仍会保留计划和清单以便追踪 |
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
主配置文件是 `~/.deepseek/config.toml`。完整选项见 [config.example.toml](config.example.toml) 和 [docs/CONFIGURATION.md](docs/CONFIGURATION.md)。
|
||||
|
||||
常用环境变量:
|
||||
|
||||
| 变量 | 用途 |
|
||||
|---|---|
|
||||
| `DEEPSEEK_API_KEY` | DeepSeek API key |
|
||||
| `DEEPSEEK_BASE_URL` | API base URL |
|
||||
| `DEEPSEEK_MODEL` | 默认模型 |
|
||||
| `DEEPSEEK_PROVIDER` | 提供方:`deepseek`、`nvidia-nim`、`fireworks` 或 `sglang` |
|
||||
| `DEEPSEEK_PROFILE` | 配置 profile 名称 |
|
||||
| `NVIDIA_API_KEY` | NVIDIA NIM API key |
|
||||
| `FIREWORKS_API_KEY` | Fireworks AI API key |
|
||||
| `SGLANG_BASE_URL` | 自托管 SGLang 端点 |
|
||||
| `SGLANG_API_KEY` | 可选 SGLang bearer token |
|
||||
|
||||
快速诊断:
|
||||
|
||||
```bash
|
||||
deepseek setup --status
|
||||
deepseek doctor --json
|
||||
```
|
||||
|
||||
UI 语言与模型输出语言相互独立。可以在 `settings.toml` 里设置 `locale`,也可以通过 `LC_ALL` / `LANG` 环境变量自动选择。支持 `en`、`zh-Hans`、`ja`、`pt-BR` 等界面语言。
|
||||
|
||||
DeepSeek 上下文缓存是自动的;当 API 返回 cache hit/miss token 字段时,TUI 会把它们纳入用量和成本统计。
|
||||
|
||||
---
|
||||
|
||||
## 模型和价格
|
||||
|
||||
DeepSeek TUI 默认面向带 100 万 token 上下文窗口的 **DeepSeek V4** 模型。
|
||||
|
||||
| 模型 | 上下文 | 输入(缓存命中) | 输入(缓存未命中) | 输出 |
|
||||
|---|---|---|---|---|
|
||||
| `deepseek-v4-pro` | 1M | $0.003625 / 1M* | $0.435 / 1M* | $0.87 / 1M* |
|
||||
| `deepseek-v4-flash` | 1M | $0.0028 / 1M | $0.14 / 1M | $0.28 / 1M |
|
||||
|
||||
旧别名 `deepseek-chat` 和 `deepseek-reasoner` 会自动映射到 `deepseek-v4-flash`。
|
||||
|
||||
**NVIDIA NIM** 托管变体(`deepseek-ai/deepseek-v4-pro`、`deepseek-ai/deepseek-v4-flash`)使用你的 NVIDIA 账号条款,不走 DeepSeek 平台计费。
|
||||
|
||||
*DeepSeek 标注的 Pro 价格是限时 75% 折扣,有效期到 2026-05-05 15:59 UTC;该时间之后 TUI 成本估算会回退到 Pro 基础价格。*
|
||||
|
||||
---
|
||||
|
||||
## 文档
|
||||
|
||||
| 文档 | 主题 |
|
||||
|---|---|
|
||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | 代码库内部结构 |
|
||||
| [CONFIGURATION.md](docs/CONFIGURATION.md) | 完整配置参考 |
|
||||
| [MODES.md](docs/MODES.md) | Plan / Agent / YOLO 模式 |
|
||||
| [MCP.md](docs/MCP.md) | Model Context Protocol 集成 |
|
||||
| [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API 服务 |
|
||||
| [RELEASE_RUNBOOK.md](docs/RELEASE_RUNBOOK.md) | 发布流程 |
|
||||
| [OPERATIONS_RUNBOOK.md](docs/OPERATIONS_RUNBOOK.md) | 运维和恢复 |
|
||||
|
||||
完整更新历史见 [CHANGELOG.md](CHANGELOG.md)。
|
||||
|
||||
---
|
||||
|
||||
## 创建和安装技能
|
||||
|
||||
DeepSeek-TUI 会从当前技能目录发现技能。优先级是:工作区
|
||||
`.agents/skills`、工作区 `./skills`、全局目录(默认
|
||||
`~/.deepseek/skills`)。每个技能都是一个包含 `SKILL.md` 的目录:
|
||||
|
||||
```text
|
||||
~/.deepseek/skills/my-skill/
|
||||
└── SKILL.md
|
||||
```
|
||||
|
||||
`SKILL.md` 需要以 YAML frontmatter 开头:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: my-skill
|
||||
description: 当 DeepSeek 需要遵循我的自定义工作流时使用这个技能。
|
||||
---
|
||||
|
||||
# My Skill
|
||||
|
||||
这里写给智能体的指令。
|
||||
```
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
/skills
|
||||
/skill my-skill
|
||||
/skill new
|
||||
/skill install github:<owner>/<repo>
|
||||
/skill update my-skill
|
||||
/skill uninstall my-skill
|
||||
/skill trust my-skill
|
||||
```
|
||||
|
||||
`/skills` 列出已发现技能,`/skill <name>` 会把技能应用到下一条消息,
|
||||
`/skill new` 会调用内置的 skill-creator 辅助创建新技能。已安装技能也会
|
||||
进入模型可见的会话上下文;当用户点名某个技能,或任务明显匹配技能描述时,
|
||||
智能体可以主动读取对应的 `SKILL.md` 并使用它。
|
||||
|
||||
社区技能可以直接从 GitHub 安装。安装过程受 `[network]` 策略约束,并会校验
|
||||
压缩包大小、路径穿越和符号链接。`/skill trust <name>` 只在你希望技能内置脚本
|
||||
可被执行时才需要。
|
||||
|
||||
---
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 pull request。请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||
|
||||
*本项目与 DeepSeek Inc. 无隶属关系。*
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -83,6 +83,8 @@ pub struct EngineConfig {
|
||||
pub notes_path: PathBuf,
|
||||
/// Path to the MCP configuration file.
|
||||
pub mcp_config_path: PathBuf,
|
||||
/// Directory containing discoverable skills.
|
||||
pub skills_dir: PathBuf,
|
||||
/// Maximum number of assistant steps before stopping.
|
||||
pub max_steps: u32,
|
||||
/// Maximum number of concurrently active subagents.
|
||||
@@ -134,6 +136,7 @@ impl Default for EngineConfig {
|
||||
trust_mode: false,
|
||||
notes_path: PathBuf::from("notes.txt"),
|
||||
mcp_config_path: PathBuf::from("mcp.json"),
|
||||
skills_dir: crate::skills::default_skills_dir(),
|
||||
max_steps: 100,
|
||||
max_subagents: DEFAULT_MAX_SUBAGENTS,
|
||||
features: Features::with_defaults(),
|
||||
@@ -1270,8 +1273,12 @@ impl Engine {
|
||||
|
||||
// Set up system prompt with project context (default to agent mode)
|
||||
let working_set_summary = session.working_set.summary_block(&config.workspace);
|
||||
let system_prompt =
|
||||
prompts::system_prompt_for_mode_with_context(AppMode::Agent, &config.workspace, None);
|
||||
let system_prompt = prompts::system_prompt_for_mode_with_context_and_skills(
|
||||
AppMode::Agent,
|
||||
&config.workspace,
|
||||
None,
|
||||
Some(&config.skills_dir),
|
||||
);
|
||||
session.system_prompt =
|
||||
append_working_set_summary(Some(system_prompt), working_set_summary.as_deref());
|
||||
|
||||
@@ -2759,7 +2766,12 @@ impl Engine {
|
||||
.session
|
||||
.working_set
|
||||
.summary_block(&self.config.workspace);
|
||||
let base = prompts::system_prompt_for_mode_with_context(mode, &self.config.workspace, None);
|
||||
let base = prompts::system_prompt_for_mode_with_context_and_skills(
|
||||
mode,
|
||||
&self.config.workspace,
|
||||
None,
|
||||
Some(&self.config.skills_dir),
|
||||
);
|
||||
let stable_prompt =
|
||||
merge_system_prompts(Some(&base), self.session.compaction_summary_prompt.clone());
|
||||
self.session.system_prompt =
|
||||
|
||||
@@ -3008,6 +3008,7 @@ async fn run_exec_agent(
|
||||
trust_mode,
|
||||
notes_path: config.notes_path(),
|
||||
mcp_config_path: config.mcp_config_path(),
|
||||
skills_dir: config.skills_dir(),
|
||||
max_steps: 100,
|
||||
max_subagents,
|
||||
features: config.features(),
|
||||
|
||||
@@ -172,6 +172,16 @@ pub fn system_prompt_for_mode_with_context(
|
||||
mode: AppMode,
|
||||
workspace: &Path,
|
||||
working_set_summary: Option<&str>,
|
||||
) -> SystemPrompt {
|
||||
system_prompt_for_mode_with_context_and_skills(mode, workspace, working_set_summary, None)
|
||||
}
|
||||
|
||||
/// Get the system prompt for a specific mode with project and skills context.
|
||||
pub fn system_prompt_for_mode_with_context_and_skills(
|
||||
mode: AppMode,
|
||||
workspace: &Path,
|
||||
working_set_summary: Option<&str>,
|
||||
skills_dir: Option<&Path>,
|
||||
) -> SystemPrompt {
|
||||
let mode_prompt = compose_mode_prompt(mode);
|
||||
|
||||
@@ -197,6 +207,11 @@ pub fn system_prompt_for_mode_with_context(
|
||||
full_prompt = format!("{full_prompt}\n\n{summary}");
|
||||
}
|
||||
|
||||
if let Some(skills_block) = skills_dir.and_then(crate::skills::render_available_skills_context)
|
||||
{
|
||||
full_prompt = format!("{full_prompt}\n\n{skills_block}");
|
||||
}
|
||||
|
||||
if let Some(handoff_block) = load_handoff_block(workspace) {
|
||||
full_prompt = format!("{full_prompt}\n\n{handoff_block}");
|
||||
}
|
||||
|
||||
@@ -1537,6 +1537,7 @@ impl RuntimeThreadManager {
|
||||
trust_mode: thread.trust_mode,
|
||||
notes_path: self.config.notes_path(),
|
||||
mcp_config_path: self.config.mcp_config_path(),
|
||||
skills_dir: self.config.skills_dir(),
|
||||
max_steps: 100,
|
||||
max_subagents: self.config.max_subagents().clamp(1, MAX_SUBAGENTS),
|
||||
features: self.config.features(),
|
||||
|
||||
@@ -22,6 +22,9 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::logging;
|
||||
|
||||
const MAX_SKILL_DESCRIPTION_CHARS: usize = 512;
|
||||
const MAX_AVAILABLE_SKILLS_CHARS: usize = 12_000;
|
||||
|
||||
// === Defaults ===
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -165,6 +168,96 @@ impl SkillRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a compact model-visible skills block.
|
||||
///
|
||||
/// The full `SKILL.md` body is intentionally not included here. This mirrors
|
||||
/// Codex's progressive-disclosure contract: the model sees skill names,
|
||||
/// descriptions, and paths up front, then opens the specific `SKILL.md` only
|
||||
/// when a skill is relevant.
|
||||
#[must_use]
|
||||
pub fn render_available_skills_context(skills_dir: &Path) -> Option<String> {
|
||||
let registry = SkillRegistry::discover(skills_dir);
|
||||
if registry.is_empty() {
|
||||
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(
|
||||
"A skill is a set of local instructions stored in a `SKILL.md` file. \
|
||||
Below is the list of skills available in this session. Each entry includes a \
|
||||
name, description, and file path so you can open the source for full \
|
||||
instructions when using a specific skill.\n\n",
|
||||
);
|
||||
out.push_str("### Available skills\n");
|
||||
|
||||
let mut omitted = 0usize;
|
||||
for skill in skills {
|
||||
let path = skills_dir.join(&skill.name).join("SKILL.md");
|
||||
let description = truncate_for_prompt(&skill.description, MAX_SKILL_DESCRIPTION_CHARS);
|
||||
let line = if description.is_empty() {
|
||||
format!("- {}: (file: {})\n", skill.name, path.display())
|
||||
} else {
|
||||
format!(
|
||||
"- {}: {} (file: {})\n",
|
||||
skill.name,
|
||||
description,
|
||||
path.display()
|
||||
)
|
||||
};
|
||||
|
||||
if out.chars().count() + line.chars().count() > MAX_AVAILABLE_SKILLS_CHARS {
|
||||
omitted += 1;
|
||||
} else {
|
||||
out.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
if omitted > 0 {
|
||||
out.push_str(&format!(
|
||||
"- ... {omitted} additional skills omitted from this prompt budget.\n"
|
||||
));
|
||||
}
|
||||
|
||||
if !registry.warnings().is_empty() {
|
||||
out.push_str("\n### Skill load warnings\n");
|
||||
for warning in registry.warnings().iter().take(8) {
|
||||
out.push_str("- ");
|
||||
out.push_str(&truncate_for_prompt(warning, MAX_SKILL_DESCRIPTION_CHARS));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str(
|
||||
"\n### How to use skills\n\
|
||||
- Discovery: The list above is the skills available in this session. Skill bodies live on disk at the listed paths.\n\
|
||||
- Trigger rules: If the user names a skill (with `$SkillName`, `/skill <name>`, or plain text) OR the task clearly matches a skill description above, use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n\
|
||||
- Missing/blocked: If a named skill is missing or its `SKILL.md` cannot be read, say so briefly and continue with the best fallback.\n\
|
||||
- Progressive disclosure: After deciding to use a skill, read only that skill's `SKILL.md`. When it references relative paths such as `scripts/foo.py`, resolve them relative to the skill directory.\n\
|
||||
- Context hygiene: Load only the specific referenced files needed for the task. Avoid bulk-loading unrelated skill resources.\n\
|
||||
- Safety: Do not execute scripts from a community skill unless the user explicitly asks or the skill has been trusted for script use.\n",
|
||||
);
|
||||
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn truncate_for_prompt(value: &str, max_chars: usize) -> String {
|
||||
let single_line = value.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if single_line.chars().count() <= max_chars {
|
||||
return single_line;
|
||||
}
|
||||
|
||||
let mut truncated = single_line
|
||||
.chars()
|
||||
.take(max_chars.saturating_sub(1))
|
||||
.collect::<String>();
|
||||
truncated.push('…');
|
||||
truncated
|
||||
}
|
||||
|
||||
// === CLI Helpers ===
|
||||
|
||||
#[allow(dead_code)] // CLI utility for future use
|
||||
@@ -202,3 +295,106 @@ pub fn show(skills_dir: &Path, name: &str) -> Result<()> {
|
||||
println!("{contents}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn create_skill_dir(tmpdir: &TempDir, skill_name: &str, skill_content: &str) {
|
||||
let skill_dir = tmpdir.path().join("skills").join(skill_name);
|
||||
std::fs::create_dir_all(&skill_dir).unwrap();
|
||||
std::fs::write(skill_dir.join("SKILL.md"), skill_content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_available_skills_context_lists_paths_and_usage() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
create_skill_dir(
|
||||
&tmpdir,
|
||||
"test-skill",
|
||||
"---\nname: test-skill\ndescription: A test skill\n---\nDo something special",
|
||||
);
|
||||
|
||||
let rendered =
|
||||
crate::skills::render_available_skills_context(&tmpdir.path().join("skills"))
|
||||
.expect("skill context");
|
||||
|
||||
assert!(rendered.contains("## Skills"));
|
||||
assert!(rendered.contains("- test-skill: A test skill"));
|
||||
assert!(rendered.contains("test-skill/SKILL.md"));
|
||||
assert!(rendered.contains("### How to use skills"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_available_skills_context_returns_none_when_empty() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let empty = tmpdir.path().join("skills");
|
||||
std::fs::create_dir_all(&empty).unwrap();
|
||||
assert!(crate::skills::render_available_skills_context(&empty).is_none());
|
||||
|
||||
let missing = tmpdir.path().join("does-not-exist");
|
||||
assert!(crate::skills::render_available_skills_context(&missing).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_available_skills_context_truncates_long_descriptions() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let long_desc = "x".repeat(2_000);
|
||||
let body = format!("---\nname: bigdesc\ndescription: {long_desc}\n---\nbody");
|
||||
create_skill_dir(&tmpdir, "bigdesc", &body);
|
||||
|
||||
let rendered =
|
||||
crate::skills::render_available_skills_context(&tmpdir.path().join("skills"))
|
||||
.expect("skill context");
|
||||
|
||||
let max = super::MAX_SKILL_DESCRIPTION_CHARS;
|
||||
assert!(rendered.contains('…'), "expected truncation marker");
|
||||
assert!(
|
||||
!rendered.contains(&"x".repeat(max + 1)),
|
||||
"untruncated long run should not appear"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_available_skills_context_collapses_internal_whitespace() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
create_skill_dir(
|
||||
&tmpdir,
|
||||
"spaced-skill",
|
||||
"---\nname: spaced-skill\ndescription: alpha \t beta gamma\n---\nbody",
|
||||
);
|
||||
|
||||
let rendered =
|
||||
crate::skills::render_available_skills_context(&tmpdir.path().join("skills"))
|
||||
.expect("skill context");
|
||||
|
||||
let line = rendered
|
||||
.lines()
|
||||
.find(|l| l.starts_with("- spaced-skill:"))
|
||||
.expect("skill line");
|
||||
assert!(line.contains("alpha beta gamma"), "got: {line:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_available_skills_context_omits_overflowing_skills() {
|
||||
let tmpdir = TempDir::new().unwrap();
|
||||
let big_desc = "y".repeat(super::MAX_SKILL_DESCRIPTION_CHARS - 20);
|
||||
for i in 0..200 {
|
||||
let body = format!("---\nname: skill-{i:03}\ndescription: {big_desc}\n---\nbody");
|
||||
create_skill_dir(&tmpdir, &format!("skill-{i:03}"), &body);
|
||||
}
|
||||
|
||||
let rendered =
|
||||
crate::skills::render_available_skills_context(&tmpdir.path().join("skills"))
|
||||
.expect("skill context");
|
||||
|
||||
assert!(
|
||||
rendered.contains("additional skills omitted from this prompt budget"),
|
||||
"expected overflow notice"
|
||||
);
|
||||
assert!(
|
||||
rendered.chars().count() < super::MAX_AVAILABLE_SKILLS_CHARS + 4_000,
|
||||
"rendered length should stay near the budget"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +355,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig {
|
||||
trust_mode: app.trust_mode,
|
||||
notes_path: config.notes_path(),
|
||||
mcp_config_path: config.mcp_config_path(),
|
||||
skills_dir: app.skills_dir.clone(),
|
||||
// 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,
|
||||
|
||||
Reference in New Issue
Block a user