소스 정보
- 저장소
- majiayu000/sage
- 최근 소스 활동
- 2026년 1월 11일 15:37
- 감지된 SKILL.md 언어
- 중국어
- 스타
- 12
- 포크
- 1
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/majiayu000/sage --skill sage-ui-design명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
SOC 직업 분류 기준
| name | sage-ui-design |
| description | Sage CLI UI 设计规范,参考 Claude Code 的终端显示模式,包含对齐、颜色、图标等设计标准 |
| when_to_use | 当修改 UI 显示、输出格式、颜色方案、动画效果时使用 |
| allowed_tools | ["Read","Grep","Glob","Edit","Write","Bash"] |
| user_invocable | true |
| priority | 90 |
参考 Claude Code 的终端 UI 设计,确保一致的用户体验。
所有内容从同一列开始,无前导空格
● 助手回复内容从这里开始
换行后的内容与上一行文字对齐(2空格缩进)
● Read(README.md)
└ Read 341 lines
✱ Thinking... (ctrl+c to interrupt)
关键规则:
● 从第 0 列开始(无前导空格)└ 树形符号,缩进 2 空格| 元素 | 颜色 | Rust 实现 | 说明 |
|---|---|---|---|
用户输入提示 > | 橙色/棕色 | .truecolor(204, 120, 50) 或 .yellow() | 用户输入区域标识 |
助手回复图标 ● | 亮白色 | .bright_white() | 内容消息标识 |
工具图标 ● | 亮蓝色 | .bright_blue() | 工具调用标识 |
| 工具名称 | 亮白色加粗 | .bright_white().bold() | 突出工具名 |
| 工具参数 | 暗淡灰色 | .dimmed() | 次要信息 |
工具结果符号 └ | 暗淡灰色 | .dimmed() | 树形结构 |
| 工具结果内容 | 暗淡灰色 | .dimmed() | 次要输出 |
| Thinking 动画 | 颜色可配置 | .bright_blue().bold() 等 | 根据状态变化 |
Thinking 完成 ✓ | 亮青色暗淡 | .bright_cyan().dimmed() | 完成状态 |
| 错误信息 | 红色 | .red() | 错误提示 |
| 成功信息 | 绿色 | .green() | 成功提示 |
| 警告信息 | 黄色 | .yellow() | 警告提示 |
// animation.rs 中的颜色映射
let colored_output = match color {
"blue" => format!("{} {}", frame, message).bright_blue().bold(),
"green" => format!("{} {}", frame, message).bright_green().bold(),
"yellow" => format!("{} {}", frame, message).bright_yellow().bold(),
"red" => format!("{} {}", frame, message).bright_red().bold(),
"cyan" => format!("{} {}", frame, message).bright_cyan().bold(),
"magenta" => format!("{} {}", frame, message).bright_magenta().bold(),
_ => format!("{} {}", frame, message).bright_white().bold(),
};
colored crateuse colored::Colorize;
// 基础颜色
"text".red() // 红色
"text".green() // 绿色
"text".yellow() // 黄色
"text".blue() // 蓝色
"text".cyan() // 青色
"text".magenta() // 品红
// 亮色变体
"text".bright_white() // 亮白色
"text".bright_blue() // 亮蓝色
"text".bright_cyan() // 亮青色
// 样式修饰
"text".bold() // 加粗
"text".dimmed() // 暗淡
"text".italic() // 斜体
// 组合使用
"text".bright_white().bold() // 亮白加粗
"text".bright_cyan().dimmed() // 亮青暗淡
// 自定义 RGB 颜色
.(, , )
// 核心图标(sage-core/src/ui/icons.rs)
pub const MESSAGE: &str = "●"; // 助手消息
pub const RESULT: &str = "└"; // 工具结果(树形)
pub const COGITATE: &str = "✱"; // Thinking 状态
pub const CHECKMARK: &str = "✓"; // 完成状态
pub const PROMPT: &str = "❯"; // 用户输入提示
● 消息内容第一行
消息内容第二行(2空格缩进)
消息内容第三行
实现:
fn on_content_start(&self) {
print!("{} ", Icons::message().bright_white());
}
fn on_content_chunk(&self, chunk: &str) {
// 换行后添加2空格缩进
let indented = chunk.replace('\n', "\n ");
print!("{}", indented);
}
● Read(README.md)
└ Read 341 lines
● Bash(git status)
└ On branch main
Your branch is up to date
● Task(探索代码库结构)
└ Found 15 files...
工具动画显示格式:
◐ Running (3.2s) · Read
◐ Running (5.1s) · 探索代码库结构
注意:
{spinner} Running ({elapsed}s) · {detail}实现:
fn on_tool_start(&self, name: &str, params: &str) {
println!();
print!("{} {}", Icons::message().bright_white(), name.bright_white().bold());
if !params.is_empty() {
println!("({})", params.dimmed());
}
}
fn on_tool_result(&self, output: &str) {
// 结果缩进2空格,多行内容额外缩进
let indented = output.replace('\n', "\n ");
println!(" {} {}", Icons::result().dimmed(), indented.dimmed());
}
进行中:
✱ Thinking... (ctrl+c to interrupt · thinking)
完成后:
✓ Thought for 2.1s
实现:
// 动画运行时(animation.rs)
async fn run_animation(...) {
let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
// 显示: "⠋ Thinking (2.3s)"
print!("\r{} {} ({:.1}s)", frame, message, elapsed);
}
// 停止时显示完成状态
pub async fn stop_animation(&self) {
let completion_msg = format!("✓ Thought for {:.1}s", elapsed);
println!("{}", completion_msg.bright_cyan().dimmed());
}
| 文件 | 职责 |
|---|---|
sage-core/src/ui/icons.rs | 图标定义 |
sage-core/src/ui/animation.rs | Thinking 动画 |
sage-core/src/output/strategy.rs | 输出策略(Streaming/Batch/JSON) |
sage-cli/src/ui/nerd_console.rs | CLI 控制台 |
修改 UI 时,确保:
└ 符号 + 2 空格缩进检查 on_content_chunk 是否正确处理换行:
let indented = chunk.replace('\n', "\n "); // 2空格
Unicode 图标在不同终端可能有不同宽度。使用 unicode-width crate 计算实际宽度:
use unicode_width::UnicodeWidthStr;
let width = icon.width();
# 运行简单测试
sage "你好"
# 检查多行输出
sage "列出这个项目的主要功能"
# 检查工具调用显示
sage "读取 README.md"
Claude Code 使用 React/Ink 实现终端 UI:
// 统一的 FlexBox 布局
<FlexBox flexDirection="column" width="100%">
<Text>{content}</Text>
</FlexBox>
// 工具显示
<FlexBox marginTop={1}>
<Text>● </Text>
<Text bold>{toolName}</Text>
<Text dimColor>({params})</Text>
</FlexBox>
<FlexBox paddingLeft={2}>
<Text dimColor>└ {result}</Text>
</FlexBox>
Sage 使用 Rust 直接输出到终端,需要手动管理缩进和对齐。
LLM API (SSE) → SseDecoder → StreamChunk → LlmOrchestrator → OutputStrategy → Terminal
SseDecoder (sage-core/src/llm/sse_decoder/mod.rs)
\n\n 分割事件Provider (sage-core/src/llm/providers/*.rs)
StreamChunkcontent_block_delta 事件中的 text_deltaLlmOrchestrator (sage-core/src/agent/unified/llm_orchestrator.rs)
output_strategy.on_content_chunk() 显示每个 chunkOutputStrategy (sage-core/src/output/strategy.rs)
StreamingOutput: 实时显示每个 chunkBatchOutput: 收集所有内容后一次显示// llm_orchestrator.rs - stream_chat_with_animation_stop
loop {
select! {
chunk_opt = stream.next() => {
match chunk_opt {
Some(Ok(chunk)) => {
if let Some(ref chunk_content) = chunk.content {
if !chunk_content.is_empty() {
// 停止动画(仅第一次)
if !animation_stopped {
event_manager.stop_animation().await;
animation_stopped = true;
}
// 开始内容显示
if !has_content {
output_strategy.on_content_start();
has_content = true;
}
// 实时显示 chunk
output_strategy.on_content_chunk(chunk_content);
content.push_str(chunk_content);
}
}
}
// ...
}
}
}
}
// 在 on_content_chunk 中添加调试
fn on_content_chunk(&self, chunk: &str) {
tracing::debug!("Received chunk: {} bytes", chunk.len());
let indented = chunk.replace('\n', "\n ");
print!("{}", indented);
let _ = io::stdout().flush();
}