ワンクリックで
sage-ui-design
Sage CLI UI 设计规范,参考 Claude Code 的终端显示模式,包含对齐、颜色、图标等设计标准
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Sage CLI UI 设计规范,参考 Claude Code 的终端显示模式,包含对齐、颜色、图标等设计标准
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
跨语言代码库健康治理与自动优化系统。基于学术研究和实战经验,系统性检测 LLM 生成代码的结构性缺陷。完整流程:SCAN → DIAGNOSE → FIX → HARDEN。适用于任何语言的项目。
Sage 到 Tink UI 框架的完全重构指南,包含迁移步骤、文件清单、架构设计
Sage 版本管理规范,包含语义化版本、CHANGELOG、发布流程
Sage Agent 执行引擎开发指南,涵盖 UnifiedExecutor、Subagent、Lifecycle 管理
Sage 项目整体架构设计指南,基于 Claude Code、Crush 最佳实践的融合方案
Sage 独有的检查点系统设计,包含状态快照、文件追踪、回滚恢复机制
| 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 颜色
"text".truecolor(204, 120, 50) // 橙色/棕色
// 核心图标(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();
}