From 55b5d1795fb218e63161a822a415d3f63433d1df Mon Sep 17 00:00:00 2001 From: bigfish Date: Tue, 8 Sep 2026 15:12:39 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(pi-tui):=20=E4=BD=BF=E7=94=A8=20pulldo?= =?UTF-8?q?wn-cmark=20=E6=9B=BF=E6=8D=A2=E5=86=85=E8=81=94=20Markdown=20?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 pulldown-cmark 依赖用于解析 CommonMark/GFM - 重构 render_inline 方法,使用 pulldown-cmark 的事件驱动解析器 - 支持更多 Markdown 特性:删除线、内联数学、任务列表等 - 改进头部渲染逻辑,隐藏 Markdown 标记符号 - 添加新的单元测试验证内联解析功能 --- Cargo.lock | 18 ++ crates/pi-tui/Cargo.toml | 3 +- crates/pi-tui/src/markdown.rs | 311 +++++++++++++++++++--------------- 3 files changed, 193 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 279f4cc..0592dfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1387,6 +1387,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + [[package]] name = "pxfm" version = "0.1.30" @@ -1773,6 +1784,7 @@ version = "0.1.5" dependencies = [ "base64", "crossterm", + "pulldown-cmark", "regex", "similar", "thiserror 1.0.69", @@ -2367,6 +2379,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/crates/pi-tui/Cargo.toml b/crates/pi-tui/Cargo.toml index bebbbd6..47b2ccc 100644 --- a/crates/pi-tui/Cargo.toml +++ b/crates/pi-tui/Cargo.toml @@ -17,10 +17,11 @@ tracing.workspace = true regex = "1" base64 = "0.22" similar = { version = "2", features = ["text", "unicode"] } +pulldown-cmark = { version = "0.12", default-features = false } [features] default = [] image-support = [] [dev-dependencies] -tokio = { workspace = true, features = ["rt", "macros"] } \ No newline at end of file +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crates/pi-tui/src/markdown.rs b/crates/pi-tui/src/markdown.rs index 29149cc..2d2b744 100644 --- a/crates/pi-tui/src/markdown.rs +++ b/crates/pi-tui/src/markdown.rs @@ -7,6 +7,8 @@ use std::any::Any; use std::sync::Mutex; +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; + use super::component::Component; use super::mermaid::Mermaid; use crate::ansi::{bold, italic, underline}; @@ -207,53 +209,35 @@ impl Markdown { } // ---- Headers ---- - // pi colors the whole heading (prefix + text) with mdHeading and - // bolds it; h1/h2 are additionally underlined. + // Headings are rendered with the mdHeading color and bold text; + // h1/h2 are additionally underlined. Markdown markers are hidden. if let Some(rest) = strip_header(line, "######") { list_continuation_indent = None; - let h = bold( - &colors - .md_heading - .fg(&format!("###### {}", self.render_inline(rest))), - ); + let h = bold(&colors.md_heading.fg(&self.render_inline(rest))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(rest) = strip_header(line, "#####") { list_continuation_indent = None; - let h = bold( - &colors - .md_heading - .fg(&format!("##### {}", self.render_inline(rest))), - ); + let h = bold(&colors.md_heading.fg(&self.render_inline(rest))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(rest) = strip_header(line, "####") { list_continuation_indent = None; - let h = bold( - &colors - .md_heading - .fg(&format!("#### {}", self.render_inline(rest))), - ); + let h = bold(&colors.md_heading.fg(&self.render_inline(rest))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(rest) = strip_header(line, "###") { list_continuation_indent = None; - // Keep the heading marker visible at every level. H1/H2 - // already do this; omitting `### ` here made H3 look like a - // regular bold paragraph and made streamed markdown jump - // between styles. - let h = bold( - &colors - .md_heading - .fg(&format!("### {}", self.render_inline(rest))), - ); + let h = bold(&colors.md_heading.fg(&self.render_inline(rest))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(rest) = strip_header(line, "##") { list_continuation_indent = None; - let inner = bold(&underline(&self.render_inline(rest))); - let h = colors.md_heading.fg(&format!("{}{}", bold("## "), inner)); + let h = colors + .md_heading + .fg(&bold(&underline(&self.render_inline(rest)))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(rest) = strip_header(line, "#") { list_continuation_indent = None; - let inner = bold(&underline(&bold(&self.render_inline(rest)))); - let h = colors.md_heading.fg(&format!("{}{}", bold("# "), inner)); + let h = colors + .md_heading + .fg(&bold(&underline(&self.render_inline(rest)))); push_wrapped(&pad, &h, cwidth, &mut lines); } else if let Some(item) = parse_list_item(line) { let marker = match item.marker { @@ -319,136 +303,161 @@ impl Markdown { lines } - /// Render inline markdown (bold, italic, code, links). + /// Render inline CommonMark/GFM with terminal-specific ANSI styling. fn render_inline(&self, text: &str) -> String { let colors = theme().colors; - let mut result = String::new(); - let mut offset = 0; - - while offset < text.len() { - let rest = &text[offset..]; - - if let Some(escaped) = rest.strip_prefix('\\') { - if let Some(ch) = escaped.chars().next() { - result.push(ch); - offset += 1 + ch.len_utf8(); - } else { - result.push('\\'); - offset += 1; + let mut options = Options::empty(); + options.insert(Options::ENABLE_STRIKETHROUGH); + + // pulldown-cmark parses complete documents and has no inline-only + // entry point. A plain prefix keeps block-looking text in inline + // context; it is discarded from the first text event below. + let source = format!("x {text}"); + let mut discard_prefix = true; + let mut frames = vec![InlineFrame::root()]; + + for event in Parser::new_ext(&source, options) { + match event { + Event::Start(Tag::Strong) => frames.push(InlineFrame::new(InlineKind::Bold)), + Event::Start(Tag::Emphasis) => frames.push(InlineFrame::new(InlineKind::Italic)), + Event::Start(Tag::Strikethrough) => { + frames.push(InlineFrame::new(InlineKind::Strike)); } - continue; - } - - if rest.starts_with('`') { - let ticks = rest.chars().take_while(|c| *c == '`').count(); - let delimiter = "`".repeat(ticks); - if let Some(end) = rest[ticks..].find(&delimiter) { - let code = &rest[ticks..ticks + end]; - let chip = format!(" {} ", colors.md_code.fg(code.trim())); - result.push_str(&colors.md_code_bg.bg(&chip)); - offset += ticks + end + ticks; - continue; + Event::Start(Tag::Link { dest_url, .. }) => { + frames.push(InlineFrame::new(InlineKind::Link(dest_url.into_string()))) } - } - - if rest.starts_with('[') { - if let Some(label_end) = rest.find("](") { - let url_start = label_end + 2; - if let Some(url_end) = matching_paren(&rest[url_start..]) { - let label = &rest[1..label_end]; - let url = &rest[url_start..url_start + url_end]; - let styled = colors.md_link.fg(&underline(&self.render_inline(label))); - result.push_str(&styled); - if label != url { - result.push_str(&colors.md_link_url.fg(&format!(" ({url})"))); - } - offset += url_start + url_end + 1; - continue; - } + Event::Start(Tag::Image { dest_url, .. }) => { + frames.push(InlineFrame::new(InlineKind::Image(dest_url.into_string()))) } - } - - if let Some((delimiter, style)) = emphasis_delimiter(rest) { - if let Some(end) = rest[delimiter.len()..].find(delimiter) { - let inner = &rest[delimiter.len()..delimiter.len() + end]; - if !inner.is_empty() { - let rendered = self.render_inline(inner); - match style { - InlineStyle::Bold => result.push_str(&bold(&rendered)), - InlineStyle::Italic => result.push_str(&italic(&rendered)), - InlineStyle::Strike => { - result.push_str("\x1b[9m"); - result.push_str(&rendered); - result.push_str("\x1b[29m"); - } - } - offset += delimiter.len() + end + delimiter.len(); - continue; + Event::End( + TagEnd::Strong + | TagEnd::Emphasis + | TagEnd::Strikethrough + | TagEnd::Link + | TagEnd::Image, + ) => close_inline_frame(&mut frames, &colors), + Event::Text(value) => { + let mut value = value.as_ref(); + if discard_prefix { + value = value.strip_prefix("x ").unwrap_or(value); + discard_prefix = false; } + frames + .last_mut() + .expect("root inline frame") + .text + .push_str(value); + } + Event::Code(code) => { + let chip = format!(" {} ", colors.md_code.fg(code.trim())); + frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&colors.md_code_bg.bg(&chip)); } + Event::InlineMath(value) => frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&format!("${value}$")), + Event::DisplayMath(value) => frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&format!("$${value}$$")), + Event::Html(value) | Event::InlineHtml(value) => frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&value), + Event::FootnoteReference(label) => frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&format!("[^{label}]")), + Event::SoftBreak => frames.last_mut().expect("root inline frame").text.push(' '), + Event::HardBreak => frames + .last_mut() + .expect("root inline frame") + .text + .push('\n'), + Event::TaskListMarker(checked) => frames + .last_mut() + .expect("root inline frame") + .text + .push(if checked { '☒' } else { '☐' }), + Event::Rule | Event::Start(_) | Event::End(_) => {} } - - let ch = rest.chars().next().expect("offset is a character boundary"); - result.push(ch); - offset += ch.len_utf8(); } - result + while frames.len() > 1 { + close_inline_frame(&mut frames, &colors); + } + frames.pop().expect("root inline frame").text } } -#[derive(Debug, Clone, Copy)] -enum InlineStyle { +#[derive(Debug)] +enum InlineKind { + Root, Bold, Italic, Strike, + Link(String), + Image(String), } -/// Return an emphasis delimiter only when it can begin a formatting span. -/// This intentionally leaves identifiers such as `snake_case` untouched. -fn emphasis_delimiter(rest: &str) -> Option<(&'static str, InlineStyle)> { - if rest.starts_with("**") { - Some(("**", InlineStyle::Bold)) - } else if rest.starts_with("__") { - Some(("__", InlineStyle::Bold)) - } else if rest.starts_with("~~") { - Some(("~~", InlineStyle::Strike)) - } else if rest.starts_with('*') { - Some(("*", InlineStyle::Italic)) - } else if rest.starts_with('_') { - let next = rest[1..].chars().next(); - if next.is_some_and(|ch| !ch.is_whitespace()) { - Some(("_", InlineStyle::Italic)) - } else { - None +#[derive(Debug)] +struct InlineFrame { + kind: InlineKind, + text: String, +} + +impl InlineFrame { + fn root() -> Self { + Self::new(InlineKind::Root) + } + + fn new(kind: InlineKind) -> Self { + Self { + kind, + text: String::new(), } - } else { - None } } -/// Return the byte offset of the close paren matching the opening paren that -/// follows a Markdown link label. Parentheses in URLs are common in docs. -fn matching_paren(text: &str) -> Option { - let mut depth = 0usize; - let mut escaped = false; - for (offset, ch) in text.char_indices() { - if escaped { - escaped = false; - continue; - } - if ch == '\\' { - escaped = true; - } else if ch == '(' { - depth += 1; - } else if ch == ')' { - if depth == 0 { - return Some(offset); +fn close_inline_frame(frames: &mut Vec, colors: &crate::theme::ThemeColors) { + let Some(frame) = (frames.len() > 1).then(|| frames.pop()).flatten() else { + return; + }; + let rendered = match frame.kind { + InlineKind::Root => frame.text, + InlineKind::Bold => bold(&frame.text), + InlineKind::Italic => italic(&frame.text), + InlineKind::Strike => format!("\x1b[9m{}\x1b[29m", frame.text), + InlineKind::Link(url) => { + let label = colors.md_link.fg(&underline(&frame.text)); + if crate::ansi::strip_ansi(&frame.text) == url { + label + } else { + format!("{label}{}", colors.md_link_url.fg(&format!(" ({url})"))) } - depth -= 1; } - } - None + InlineKind::Image(url) => { + let alt = if frame.text.is_empty() { + "image".to_string() + } else { + frame.text + }; + format!("{alt}{}", colors.md_link_url.fg(&format!(" ({url})"))) + } + }; + frames + .last_mut() + .expect("root inline frame") + .text + .push_str(&rendered); } enum ListMarker { @@ -805,9 +814,35 @@ mod tests { #[test] fn test_markdown_headers() { - let md = Markdown::new("# Title\n## Subtitle\n### Heading", 0, 0); + let md = Markdown::new("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6", 0, 0); let lines = md.render(80); - assert_eq!(lines.len(), 3); + let plain: Vec = lines + .iter() + .map(|line| crate::ansi::strip_ansi(line)) + .collect(); + assert_eq!(plain, vec!["H1", "H2", "H3", "H4", "H5", "H6"]); + } + + #[test] + fn test_commonmark_inline_parser_handles_nested_spans_and_images() { + let md = Markdown::new( + "This is ***nested***, ``a ` b``, , ![plot](plot.png)", + 0, + 0, + ); + let rendered = md.render(120).join("\n"); + let plain = crate::ansi::strip_ansi(&rendered); + + assert_eq!( + plain, + "This is nested, a ` b , https://example.com, plot (plot.png)" + ); + assert!(rendered.contains("\x1b[1m"), "nested strong style missing"); + assert!(rendered.contains("\x1b[3m"), "nested emphasis missing"); + assert!( + rendered.contains("\x1b[48;2;48;52;59m"), + "multi-backtick code span should use the code chip" + ); } #[test] From 7138600cb950241b6badecd3db61c316efdc6714 Mon Sep 17 00:00:00 2001 From: bigfish Date: Tue, 8 Sep 2026 15:19:41 +0800 Subject: [PATCH 2/4] release v0.1.6 --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 18 +++++++++--------- examples/minimal/Cargo.toml | 2 +- examples/plugin-stub/Cargo.toml | 2 +- examples/tools/Cargo.toml | 2 +- examples/tui/Cargo.toml | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0592dfa..e9fe1e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1044,7 +1044,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "minimal" -version = "0.1.5" +version = "0.1.6" dependencies = [ "futures", "rpi-agent", @@ -1344,7 +1344,7 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "plugin-stub" -version = "0.1.5" +version = "0.1.6" dependencies = [ "rpi-plugin-sdk", "serde_json", @@ -1631,7 +1631,7 @@ dependencies = [ [[package]] name = "rpi-agent" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "futures", @@ -1648,7 +1648,7 @@ dependencies = [ [[package]] name = "rpi-ai" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "base64", @@ -1670,7 +1670,7 @@ dependencies = [ [[package]] name = "rpi-cli" -version = "0.1.5" +version = "0.1.6" dependencies = [ "arboard", "async-trait", @@ -1700,7 +1700,7 @@ dependencies = [ [[package]] name = "rpi-extensions" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "futures", @@ -1718,7 +1718,7 @@ dependencies = [ [[package]] name = "rpi-harness" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "futures", @@ -1738,14 +1738,14 @@ dependencies = [ [[package]] name = "rpi-plugin-sdk" -version = "0.1.5" +version = "0.1.6" dependencies = [ "serde_json", ] [[package]] name = "rpi-telemetry" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "tokio", @@ -1753,7 +1753,7 @@ dependencies = [ [[package]] name = "rpi-tools" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-trait", "base64", @@ -1780,7 +1780,7 @@ dependencies = [ [[package]] name = "rpi-tui" -version = "0.1.5" +version = "0.1.6" dependencies = [ "base64", "crossterm", @@ -2278,7 +2278,7 @@ dependencies = [ [[package]] name = "tools-example" -version = "0.1.5" +version = "0.1.6" dependencies = [ "futures", "rpi-agent", @@ -2372,7 +2372,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tui-example" -version = "0.1.5" +version = "0.1.6" dependencies = [ "crossterm", "rpi-tui", diff --git a/Cargo.toml b/Cargo.toml index 8b0773c..5b3c392 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ edition = "2021" rust-version = "1.78" license = "MIT" repository = "https://github.com/bigfish193/pi-rust" -version = "0.1.5" +version = "0.1.6" authors = ["BigFish "] [workspace.dependencies] @@ -65,16 +65,16 @@ rpassword = "7" # pi-rust internal crates (published under the `rpi-` prefix; directories keep # the `pi-` name — package `name` is what crates.io + `extern crate` see). -rpi-telemetry = { path = "crates/pi-telemetry", version = "0.1.5" } -rpi-ai = { path = "crates/pi-ai", version = "0.1.5" } -rpi-agent = { path = "crates/pi-agent", version = "0.1.5" } -rpi-tools = { path = "crates/pi-tools", version = "0.1.5" } -rpi-harness = { path = "crates/pi-harness", version = "0.1.5" } +rpi-telemetry = { path = "crates/pi-telemetry", version = "0.1.6" } +rpi-ai = { path = "crates/pi-ai", version = "0.1.6" } +rpi-agent = { path = "crates/pi-agent", version = "0.1.6" } +rpi-tools = { path = "crates/pi-tools", version = "0.1.6" } +rpi-harness = { path = "crates/pi-harness", version = "0.1.6" } -rpi-tui = { path = "crates/pi-tui", version = "0.1.5" } +rpi-tui = { path = "crates/pi-tui", version = "0.1.6" } -rpi-plugin-sdk = { path = "crates/rpi-plugin-sdk", version = "0.1.5" } -rpi-extensions = { path = "crates/rpi-extensions", version = "0.1.5" } +rpi-plugin-sdk = { path = "crates/rpi-plugin-sdk", version = "0.1.6" } +rpi-extensions = { path = "crates/rpi-extensions", version = "0.1.6" } [profile.release] lto = "thin" diff --git a/examples/minimal/Cargo.toml b/examples/minimal/Cargo.toml index 59e5534..181e820 100644 --- a/examples/minimal/Cargo.toml +++ b/examples/minimal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "minimal" -version = "0.1.5" +version = "0.1.6" edition = "2021" publish = false diff --git a/examples/plugin-stub/Cargo.toml b/examples/plugin-stub/Cargo.toml index 0022612..6e2a304 100644 --- a/examples/plugin-stub/Cargo.toml +++ b/examples/plugin-stub/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "plugin-stub" -version = "0.1.5" +version = "0.1.6" edition = "2021" publish = false description = "Minimal rpi Rust-cdylib plugin: registers an `echo` tool + one event handler (the Part B2 ABI smoke target)." diff --git a/examples/tools/Cargo.toml b/examples/tools/Cargo.toml index bdaab51..b5653ea 100644 --- a/examples/tools/Cargo.toml +++ b/examples/tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tools-example" -version = "0.1.5" +version = "0.1.6" edition = "2021" publish = false diff --git a/examples/tui/Cargo.toml b/examples/tui/Cargo.toml index 39da4d6..c0ad5e1 100644 --- a/examples/tui/Cargo.toml +++ b/examples/tui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tui-example" -version = "0.1.5" +version = "0.1.6" edition = "2021" publish = false From 2a74d593f038894b1b9c65220ddde3dedd5ca24f Mon Sep 17 00:00:00 2001 From: bigfish Date: Wed, 9 Sep 2026 23:06:48 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=92=8C=E5=89=8D=E7=AB=AF=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 rpi-plugin-sdk 提供稳定的 C ABI 接口用于 Rust 原生插件开发 - 新增 rpi-extensions 实现动态插件加载器和 AgentTool 适配器 - 新增 rpi-tui 包含终端 UI 原语组件 - 更新 README.md 添加插件构建说明和 crate 注册表链接 - 添加 Taskfile.yml 中的前端打包和部署任务 - 在 .gitignore 中添加静态前端部署包目录 fix: 修复 Anthropic 流式传输和 TUI 渲染问题 - 为 Anthropic 提供器添加非流式请求支持,通过 RPI_ANTHROPIC_NON_STREAM 环境变量控制 - 修复 TUI 中光标标记导致的终端渲染问题,确保光标标记不会写入实际终端 - 优化交互式 TUI 中的消息队列机制,改进用户输入处理逻辑 - 修复 bash 工具调用时的工作状态加载器同步显示问题 --- .gitignore | 3 + .issue-assets/copy.html | 23 +++ .issue-assets/rpi-interactive.png | Bin 0 -> 50006 bytes .issue-assets/rpi-working.png | Bin 0 -> 110628 bytes README.md | 47 ++++- Taskfile.yml | 81 +++++++++ crates/pi-ai/src/providers/anthropic/mod.rs | 160 ++++++++++++++++- crates/pi-cli/src/interactive_tui.rs | 70 ++++---- crates/pi-tui/Cargo.toml | 3 + crates/pi-tui/src/tui_alt_screen.rs | 39 +++- deploy/caddy-rpi.override.yml | 10 ++ deploy/rpi.caddy | 13 ++ docs/promotion-kit.md | 177 ++++++++++++++++++ docs/release-v0.1.6.md | 26 +++ website/README.md | 11 ++ website/app.js | 85 +++++++++ website/data/docs.json | 188 ++++++++++++++++++++ website/data/packages.json | 74 ++++++++ website/data/site.json | 47 +++++ website/docs.html | 44 +++++ website/docs.js | 64 +++++++ website/index.html | 62 +++++++ website/packages.html | 44 +++++ website/packages.js | 62 +++++++ website/styles.css | 53 ++++++ 25 files changed, 1344 insertions(+), 42 deletions(-) create mode 100644 .issue-assets/copy.html create mode 100644 .issue-assets/rpi-interactive.png create mode 100644 .issue-assets/rpi-working.png create mode 100644 deploy/caddy-rpi.override.yml create mode 100644 deploy/rpi.caddy create mode 100644 docs/promotion-kit.md create mode 100644 docs/release-v0.1.6.md create mode 100644 website/README.md create mode 100644 website/app.js create mode 100644 website/data/docs.json create mode 100644 website/data/packages.json create mode 100644 website/data/site.json create mode 100644 website/docs.html create mode 100644 website/docs.js create mode 100644 website/index.html create mode 100644 website/packages.html create mode 100644 website/packages.js create mode 100644 website/styles.css diff --git a/.gitignore b/.gitignore index f949afc..5204ca9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ task-*.log release-*.log .claude/* target/* + +# Static frontend deployment bundle +.deploy/ diff --git a/.issue-assets/copy.html b/.issue-assets/copy.html new file mode 100644 index 0000000..30f5e65 --- /dev/null +++ b/.issue-assets/copy.html @@ -0,0 +1,23 @@ + + +Copy screenshot + + + + diff --git a/.issue-assets/rpi-interactive.png b/.issue-assets/rpi-interactive.png new file mode 100644 index 0000000000000000000000000000000000000000..8a4dbd3f1c693c78602d3970e501feb18b1bdf11 GIT binary patch literal 50006 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 D)P>8M literal 0 HcmV?d00001 diff --git a/.issue-assets/rpi-working.png b/.issue-assets/rpi-working.png new file mode 100644 index 0000000000000000000000000000000000000000..8615868074a23636e375187fa8f8ed61a5ee32cc GIT binary patch literal 110628 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

;JMYOmc?13jph=c&abc^gn6Ol@P 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 E0F>t~ZU6uP literal 0 HcmV?d00001 diff --git a/README.md b/README.md index a5d6619..fc31393 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # rpi — Rust port of the Pi agent SDK +[![rpi-cli on crates.io](https://img.shields.io/crates/v/rpi-cli.svg)](https://crates.io/crates/rpi-cli) +[![rpi-plugin-sdk docs](https://docs.rs/rpi-plugin-sdk/badge.svg)](https://docs.rs/rpi-plugin-sdk) +[![CI](https://github.com/bigfish1913/pi-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/bigfish1913/pi-rust/actions) + A Rust port of [earendil-works/pi](https://github.com/earendil-works/pi)'s SDK layer — a library-first, multi-crate workspace for building personal LLM coding agents in Rust, plus an `rpi` CLI built on top. @@ -19,9 +23,25 @@ agents in Rust, plus an `rpi` CLI built on top. | `rpi-tools` | `pi-tools/` | Built-in tools (`read`/`write`/`edit`/`bash`/`grep`/`find`/`ls`) + `ExecutionEnv`. | | `rpi-harness` | `pi-harness/` | `AgentHarness`: session tree, JSONL persistence, compaction, run loop. | | `rpi-cli` | `pi-cli/` | Terminal coding-agent CLI (`rpi` binary) on top of the library crates. | +| `rpi-plugin-sdk` | `rpi-plugin-sdk/` | Stable C ABI for Rust-native plugins and extension discovery. | +| `rpi-extensions` | `rpi-extensions/` | Dynamic plugin loader and `AgentTool` adapter. | +| `rpi-tui` | `pi-tui/` | Terminal UI primitives used by the interactive CLI. | Dependency direction: `rpi-telemetry → rpi-ai → rpi-agent → rpi-tools → rpi-harness → rpi-cli`. +### Rust registry links + +| Package | crates.io | docs.rs | +| --- | --- | --- | +| `rpi-cli` | [crates.io](https://crates.io/crates/rpi-cli) | [docs.rs](https://docs.rs/rpi-cli) | +| `rpi-agent` | [crates.io](https://crates.io/crates/rpi-agent) | [docs.rs](https://docs.rs/rpi-agent) | +| `rpi-plugin-sdk` | [crates.io](https://crates.io/crates/rpi-plugin-sdk) | [docs.rs](https://docs.rs/rpi-plugin-sdk) | +| `rpi-extensions` | [crates.io](https://crates.io/crates/rpi-extensions) | [docs.rs](https://docs.rs/rpi-extensions) | + +The registry pages are the canonical entry points for installing the CLI or +embedding the SDK. The repository may contain unreleased changes; check the +published version shown on crates.io before depending on a new API. + ## Relationship to the TypeScript source The TypeScript reference is checked out under `.reference/pi/` (read-only). Every @@ -55,6 +75,29 @@ agent.prompt("Hello!").await.unwrap(); See [docs/architecture.md](docs/architecture.md) for the full design. +## Build a plugin + +Plugins are Rust `cdylib` libraries loaded through the stable ABI exposed by +`rpi-plugin-sdk`. The repository includes a complete `echo` tool example that +also exercises event and resource discovery: + +```bash +cargo build -p plugin-stub +``` + +Then point the CLI at the directory containing the generated library (the +extension is named `plugin_stub.dll`, `libplugin_stub.so`, or +`libplugin_stub.dylib` depending on the platform): + +```bash +rpi --extensions-dir target/debug -p 'echo "hi"' +``` + +The plugin depends on `rpi-plugin-sdk` only; the host-side loader lives in +`rpi-extensions`. See [`examples/plugin-stub`](examples/plugin-stub) and the +[`rpi-plugin-sdk` API docs](https://docs.rs/rpi-plugin-sdk) for the ABI +contract. + ## Status (v1) - **Providers:** Anthropic Messages and OpenAI-compatible Chat Completions, @@ -131,8 +174,8 @@ Publish the crate family in dependency order with `cargo publish` (run publish-scoped token; crates.io records are permanent): ``` -# dep order: telemetry → ai → agent → tools → harness → cli -for c in rpi-telemetry rpi-ai rpi-agent rpi-tools rpi-harness rpi-cli; do +# dep order: telemetry → ai → agent → tools → harness → plugin-sdk → extensions → tui → cli +for c in rpi-telemetry rpi-ai rpi-agent rpi-tools rpi-harness rpi-plugin-sdk rpi-extensions rpi-tui rpi-cli; do cargo publish -p "$c" done ``` diff --git a/Taskfile.yml b/Taskfile.yml index 8b371ab..cf42fed 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -22,6 +22,11 @@ version: '3' +vars: + RPI_SERVICE_NAME: rpi + RPI_DOMAIN: rpi.laofu.online + RPI_DEPLOY_DIR: .deploy/rpi + tasks: default: desc: List available release targets @@ -72,3 +77,79 @@ tasks: desc: cargo test --workspace (the release pre-flight, run standalone) cmds: - cargo test --workspace + + rpi-pack-frontend: + desc: 打包 rpi 静态官网 + cmds: + - >- + powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path '{{.RPI_DEPLOY_DIR}}' | Out-Null; Compress-Archive -Path 'website\\*' -DestinationPath '{{.RPI_DEPLOY_DIR}}\\frontend.zip' -Force" + - >- + echo "前端包已生成: {{.RPI_DEPLOY_DIR}}/frontend.zip" + + rpi-upload-frontend: + desc: 上传 rpi 静态官网到公网服务器 + deps: + - rpi-pack-frontend + cmds: + - ssh root@139.196.85.175 "mkdir -p /var/www/{{.RPI_SERVICE_NAME}} /tmp/deploy" + - scp "{{.RPI_DEPLOY_DIR}}/frontend.zip" root@139.196.85.175:/tmp/deploy/{{.RPI_SERVICE_NAME}}-frontend.zip + - ssh root@139.196.85.175 "unzip -o /tmp/deploy/{{.RPI_SERVICE_NAME}}-frontend.zip -d /var/www/{{.RPI_SERVICE_NAME}} >/dev/null && rm /tmp/deploy/{{.RPI_SERVICE_NAME}}-frontend.zip" + - >- + echo "前端文件已上传到 /var/www/{{.RPI_SERVICE_NAME}}/" + + rpi-caddy-upload: + desc: 上传 rpi Caddy 配置并重载 + cmds: + - scp deploy/rpi.caddy root@139.196.85.175:/root/work/ai-infr/config/caddy/sites/{{.RPI_SERVICE_NAME}}.caddy + - ssh root@139.196.85.175 "docker exec app-caddy sh -c 'mkdir -p /data/caddy && chmod 755 /data/caddy'" + - ssh root@139.196.85.175 "docker exec app-caddy caddy validate --config /etc/caddy/Caddyfile && docker exec app-caddy caddy reload --config /etc/caddy/Caddyfile" + - >- + echo "Caddy 配置已生效: {{.RPI_DOMAIN}}" + + rpi-caddy-mount: + desc: 将 rpi 静态目录挂载到现有 Caddy 容器 + cmds: + - scp deploy/caddy-rpi.override.yml root@139.196.85.175:/root/work/ai-infr/docker-compose.rpi.override.yml + - ssh root@139.196.85.175 "cd /root/work/ai-infr && docker compose -f docker-compose.yml -f docker-compose.rpi.override.yml config >/dev/null" + - ssh root@139.196.85.175 "docker rm -f app-caddy 2>/dev/null || true" + - ssh root@139.196.85.175 "cd /root/work/ai-infr && docker compose -f docker-compose.yml -f docker-compose.rpi.override.yml up -d caddy" + - powershell -NoProfile -Command "Start-Sleep -Seconds 5" + + rpi-caddy-storage-repair: + desc: 恢复 Caddy 缺失的绑定存储目录并重启容器 + cmds: + - ssh root@139.196.85.175 "mkdir -p /var/lib/docker/volumes/app-caddy-data/_data /var/lib/docker/volumes/app-caddy-config/_data" + - ssh root@139.196.85.175 "docker restart app-caddy" + - powershell -NoProfile -Command "Start-Sleep -Seconds 5" + + rpi-deploy: + desc: 打包、上传并发布 rpi 静态官网 + cmds: + - task: rpi-upload-frontend + - task: rpi-caddy-mount + - task: rpi-caddy-upload + - task: rpi-verify + + rpi-verify: + desc: 验证 rpi 公网首页和静态 JSON + cmds: + - ssh root@139.196.85.175 "curl --fail --silent --show-error --head https://{{.RPI_DOMAIN}}/" + - ssh root@139.196.85.175 "curl --fail --silent --show-error https://{{.RPI_DOMAIN}}/data/packages.json" + - ssh root@139.196.85.175 "curl --fail --silent --show-error https://{{.RPI_DOMAIN}}/index.html | grep -F 'bigfish1913/pi-rust'" + + rpi-diagnose: + desc: 诊断 rpi 公网 HTTP 与 Caddy 状态 + cmds: + - ssh root@139.196.85.175 "curl --max-time 15 --silent --show-error --head http://{{.RPI_DOMAIN}}/" + - ssh root@139.196.85.175 "docker logs app-caddy --tail 80 2>&1 | grep -i '{{.RPI_DOMAIN}}' || true" + - ssh root@139.196.85.175 "docker inspect app-caddy | head -c 5000" + - ssh root@139.196.85.175 "docker exec app-caddy sh -c 'ls -ld /data /config /etc/caddy 2>&1 || true'" + - ssh root@139.196.85.175 "docker exec app-caddy sh -c 'id; stat /data /config; test -w /config && echo config-writable || echo config-not-writable'" + - ssh root@139.196.85.175 "docker volume inspect app-caddy-data app-caddy-config || true" + - ssh root@139.196.85.175 "ls -ld /var/lib/docker/volumes/app-caddy-data/_data /var/lib/docker/volumes/app-caddy-config/_data 2>&1 || true; ls -la /var/lib/docker/volumes/app-caddy-data/_data /var/lib/docker/volumes/app-caddy-config/_data 2>&1 || true" + - ssh root@139.196.85.175 "docker info | grep -E 'Docker Root Dir|Storage Driver'; ls -ld /var/lib/docker /var/lib/docker/volumes" + - ssh root@139.196.85.175 "find /var/www/rpi -maxdepth 2 -type f -printf '%P\\n' | sort | head -80" + - ssh root@139.196.85.175 "curl --insecure --silent --show-error --head https://{{.RPI_DOMAIN}}/index.html; curl --insecure --silent --show-error https://{{.RPI_DOMAIN}}/data/packages.json" + - ssh root@139.196.85.175 "find /root/work/ai-infr -maxdepth 4 -type f \( -name 'docker-compose*.yml' -o -name 'compose*.yml' \) -print" + - ssh root@139.196.85.175 "grep -R -n 'app-caddy' /root/work/ai-infr --include='*.yml' --include='*.yaml' 2>/dev/null | head -40" + - ssh root@139.196.85.175 "sed -n '170,230p' /root/work/ai-infr/docker-compose.yml" diff --git a/crates/pi-ai/src/providers/anthropic/mod.rs b/crates/pi-ai/src/providers/anthropic/mod.rs index 8d19e03..41b281d 100644 --- a/crates/pi-ai/src/providers/anthropic/mod.rs +++ b/crates/pi-ai/src/providers/anthropic/mod.rs @@ -27,7 +27,7 @@ use crate::event_stream::{ }; use crate::model::{Model, StreamingProtocolCompat}; use crate::provider::{CacheRetention, Provider, SimpleStreamOptions}; -use crate::types::{Context, Usage}; +use crate::types::{AssistantMessageEvent, Content, Context, DoneReason, StopReason, Usage}; use async_trait::async_trait; use std::sync::Arc; @@ -189,7 +189,7 @@ async fn run_anthropic_stream( // ---- build params (is_oauth = false in v1) ---- let built = build_params(model, ctx, false, opts); - let body = match serde_json::to_value(&built.request) { + let mut body = match serde_json::to_value(&built.request) { Ok(v) => v, Err(e) => { emit_terminal_error( @@ -201,6 +201,15 @@ async fn run_anthropic_stream( return; } }; + let non_stream = std::env::var("RPI_ANTHROPIC_NON_STREAM") + .ok() + .as_deref() + == Some("1"); + if non_stream { + body["stream"] = serde_json::Value::Bool(false); + strip_cache_control(&mut body); + simplify_non_stream_request(&mut body); + } // ---- assemble headers (createClient, API-key arm) ---- let headers = assemble_headers( @@ -270,11 +279,103 @@ async fn run_anthropic_stream( Ok(r) => r, Err(err) => { let aborted = err.is_abort(); + eprintln!("anthropic request failed: {err}"); emit_terminal_error(prod, &mut state, err.to_string(), aborted); return; } }; + if non_stream { + let response_body = match response.text().await { + Ok(body) => body, + Err(error) => { + emit_terminal_error( + prod, + &mut state, + format!("failed to read non-stream response: {error}"), + false, + ); + return; + } + }; + let value: serde_json::Value = match serde_json::from_str(&response_body) { + Ok(value) => value, + Err(error) => { + emit_terminal_error( + prod, + &mut state, + format!("failed to parse non-stream response: {error}"), + false, + ); + return; + } + }; + let text = value + .get("content") + .and_then(serde_json::Value::as_array) + .map(|blocks| { + blocks + .iter() + .filter(|block| block.get("type").and_then(serde_json::Value::as_str) == Some("text")) + .filter_map(|block| block.get("text").and_then(serde_json::Value::as_str)) + .collect::() + }) + .unwrap_or_default(); + if text.trim().is_empty() { + emit_terminal_error( + prod, + &mut state, + "non-stream response contained no text content".into(), + false, + ); + return; + } + state.output.content.push(Content::text(text.clone())); + state.output.response_id = value + .get("id") + .and_then(serde_json::Value::as_str) + .map(str::to_string); + state.output.response_model = value + .get("model") + .and_then(serde_json::Value::as_str) + .map(str::to_string); + if let Some(usage) = value.get("usage") { + state.output.usage.input = usage + .get("input_tokens") + .and_then(serde_json::Value::as_i64) + .unwrap_or(0); + state.output.usage.output = usage + .get("output_tokens") + .and_then(serde_json::Value::as_i64) + .unwrap_or(0); + state.output.usage.total_tokens = state.output.usage.input + state.output.usage.output; + } + state.output.stop_reason = StopReason::Stop; + let partial = std::sync::Arc::new(state.output.clone()); + prod.push(AssistantMessageEvent::Start { + partial: partial.clone(), + }); + prod.push(AssistantMessageEvent::TextStart { + content_index: 0, + partial: partial.clone(), + }); + prod.push(AssistantMessageEvent::TextDelta { + content_index: 0, + delta: text.clone(), + partial: partial.clone(), + }); + prod.push(AssistantMessageEvent::TextEnd { + content_index: 0, + content: text, + partial, + }); + prod.push(AssistantMessageEvent::Done { + reason: DoneReason::Stop, + message: state.output, + }); + return; + } + // ---- push start, drive SSE → mapper → terminal ---- // The TS pushes `start` here right before the event loop; the Rust mapper // defers `start` to the first event (`ensure_started`) so a pre-stream @@ -287,6 +388,51 @@ async fn run_anthropic_stream( run_mapper(&mut sse, prod, &mut state, cost_fn).await; } +fn strip_cache_control(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(map) => { + map.remove("cache_control"); + for child in map.values_mut() { + strip_cache_control(child); + } + } + serde_json::Value::Array(items) => { + for child in items { + strip_cache_control(child); + } + } + _ => {} + } +} + +fn simplify_non_stream_request(value: &mut serde_json::Value) { + let text_from_blocks = |blocks: &serde_json::Value| { + blocks + .as_array() + .map(|items| { + items + .iter() + .filter_map(|item| item.get("text").and_then(serde_json::Value::as_str)) + .collect::() + }) + .unwrap_or_default() + }; + if let Some(messages) = value.get_mut("messages").and_then(serde_json::Value::as_array_mut) { + for message in messages { + if let Some(content) = message.get_mut("content") { + if content.is_array() { + *content = serde_json::Value::String(text_from_blocks(content)); + } + } + } + } + if let Some(system) = value.get_mut("system") { + if system.is_array() { + *system = serde_json::Value::String(text_from_blocks(system)); + } + } +} + // ---------------------------------------------------------------------------- // Header + auth helpers — mirrors createClient (API-key arm) + assertRequestAuth. // ---------------------------------------------------------------------------- @@ -333,10 +479,12 @@ fn assemble_headers( ) -> Vec<(String, String)> { let mut headers: Vec<(String, String)> = Vec::new(); headers.push(("accept".into(), "application/json".into())); - headers.push(( - "anthropic-dangerous-direct-browser-access".into(), - "true".into(), - )); + if std::env::var("RPI_ANTHROPIC_NON_STREAM").ok().as_deref() != Some("1") { + headers.push(( + "anthropic-dangerous-direct-browser-access".into(), + "true".into(), + )); + } headers.push(("anthropic-version".into(), ANTHROPIC_VERSION.into())); if let Some(beta) = beta_header { headers.push(("anthropic-beta".into(), beta.into())); diff --git a/crates/pi-cli/src/interactive_tui.rs b/crates/pi-cli/src/interactive_tui.rs index 1a950ac..9dff087 100644 --- a/crates/pi-cli/src/interactive_tui.rs +++ b/crates/pi-cli/src/interactive_tui.rs @@ -1287,10 +1287,6 @@ fn register_extension_commands( /// main async loop. enum TuiMessage { UserInput(String), - QueueInput { - prompt: String, - follow_up: bool, - }, OpenTree, NavigateTree(String), Exit, @@ -3072,10 +3068,27 @@ pub async fn interactive_tui( return; } - if *ctx_for_cb.state.status.lock().unwrap() != RunStatus::Idle { - let _ = ctx_for_cb.tx.send(TuiMessage::QueueInput { - prompt: text.to_string(), - follow_up: false, + let run_status = *ctx_for_cb.state.status.lock().unwrap(); + if run_status != RunStatus::Idle { + let message = AgentMessage::User(UserMessage::new(text.to_string(), 0)); + let aborting = run_status == RunStatus::Aborting; + let lane = ctx_for_cb.lane.clone(); + let chat = ctx_for_cb.chat.clone(); + let tui = ctx_for_cb.tui.clone(); + tokio::spawn(async move { + // Queue immediately while the agent loop is still running. + // Routing this through the TUI's main channel delayed it until + // `prompt_text()` returned, after the loop's drain points had + // passed, so the queued message appeared to disappear. + let result = if aborting { + lane.next_run(message).await + } else { + lane.steer(message).await + }; + if let Err(error) = result { + add_error_message(&chat, &format!("Could not queue message: {error}")); + tui.request_render(false); + } }); add_note_message( &ctx_for_cb.chat, @@ -3509,9 +3522,15 @@ pub async fn interactive_tui( &state_for_key.chat_container, &format!("Queued follow-up message: {prompt}"), ); - let _ = tx_for_key.send(TuiMessage::QueueInput { - prompt, - follow_up: true, + let message = AgentMessage::User(UserMessage::new(prompt, 0)); + let lane = lane_for_key.clone(); + let chat = state_for_key.chat_container.clone(); + let tui = tui_for_key.clone(); + tokio::spawn(async move { + if let Err(error) = lane.follow_up(message).await { + add_error_message(&chat, &format!("Could not queue message: {error}")); + tui.request_render(false); + } }); } tui_for_key.request_render(false); @@ -3556,26 +3575,6 @@ pub async fn interactive_tui( editor.clear(); run_prompt_streaming(&lane, &prompt, &tui, &state, drain_handle.is_some()).await; } - Some(TuiMessage::QueueInput { prompt, follow_up }) => { - let message = AgentMessage::User(UserMessage::new(prompt.clone(), 0)); - let aborting = *state.status.lock().unwrap() == RunStatus::Aborting; - let result = if aborting { - // An aborting run will not reach either agent-loop drain - // point, so preserve the message for the next run. - lane.next_run(message).await - } else if follow_up { - lane.follow_up(message).await - } else { - lane.steer(message).await - }; - if let Err(error) = result { - add_error_message( - &chat_container, - &format!("Could not queue message: {error}"), - ); - tui.request_render(false); - } - } Some(TuiMessage::OpenTree) => { if *state.status.lock().unwrap() != RunStatus::Idle { add_note_message( @@ -4057,6 +4056,7 @@ async fn handle_agent_event( } => { if let AgentMessage::Assistant(a) = &message { let text = assistant_text(a); + let mut saw_bash_tool_call = false; // Scan content for finalized tool calls → proactively create // tool components (TS shows the tool as soon as the assistant // emits the ToolCall; ToolExecutionStart coalesces if it @@ -4064,6 +4064,7 @@ async fn handle_agent_event( for c in &a.content { if let Content::ToolCall(tc) = c { if tc.name == "bash" { + saw_bash_tool_call = true; // Bash has a dedicated component. Create it here as // well as on ToolExecutionStart because the tool // call can become visible in a MessageUpdate first. @@ -4094,6 +4095,13 @@ async fn handle_agent_event( } } } + // MessageUpdate can expose the finalized bash call before + // ToolExecutionStart arrives. Hide the global `Working…` + // loader immediately when creating that bash panel; otherwise + // it briefly appears alongside the panel's `Running…` spinner. + if saw_bash_tool_call { + state.sync_working_loader_with_bash(); + } let _ = assistant_message_event; // snapshot already applied via `a` if let Some(comp) = state.current_assistant.lock().unwrap().as_ref() { // Stream the full block list (text + thinking) each update diff --git a/crates/pi-tui/Cargo.toml b/crates/pi-tui/Cargo.toml index 47b2ccc..b893d61 100644 --- a/crates/pi-tui/Cargo.toml +++ b/crates/pi-tui/Cargo.toml @@ -7,6 +7,9 @@ license.workspace = true authors.workspace = true repository.workspace = true description = "Terminal User Interface for pi-rust" +readme = "../../README.md" +keywords = ["agent", "llm", "ai", "terminal", "tui"] +categories = ["command-line-interface", "api-bindings"] [dependencies] crossterm = { version = "0.27", features = ["events"] } diff --git a/crates/pi-tui/src/tui_alt_screen.rs b/crates/pi-tui/src/tui_alt_screen.rs index d3273cf..de8cd61 100644 --- a/crates/pi-tui/src/tui_alt_screen.rs +++ b/crates/pi-tui/src/tui_alt_screen.rs @@ -424,6 +424,15 @@ impl TuiAltScreen { let mut visible = frame.lines; self.paint_overlays(&mut visible, width, height); + // Cursor markers are an internal layout protocol, not terminal output. + // Extract the location first, then strip every marker before diffing or + // writing. Sending the APC marker to some Windows terminals caused the + // character under the cursor to be erased when moving left/right. + let cursor = extract_cursor_position(&visible, height); + for line in &mut visible { + *line = line.replace(CURSOR_MARKER, ""); + } + // Check for full redraw let previous = self .previous_screen @@ -454,8 +463,9 @@ impl TuiAltScreen { buffer.push_str(&format!("\x1b[{};1H\x1b[2K{}", row + 1, line)); } - // Find cursor position if present - if let Some((row, col)) = extract_cursor_position(&visible, height) { + // Position the hardware cursor using the marker location captured + // before internal markers were stripped from `visible`. + if let Some((row, col)) = cursor { buffer.push_str(&format!("\x1b[{};{}H", row + 1, col + 1)); if self.get_show_hardware_cursor() { buffer.push_str("\x1b[?25h"); @@ -885,7 +895,8 @@ impl Terminal for TerminalProxy { #[cfg(test)] mod tests { use super::*; - use crate::Text; + use crate::{Editor, Focusable, Text}; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; struct RecordingTerminal { output: Arc>, @@ -927,6 +938,28 @@ mod tests { fn flush(&self) {} } + #[test] + fn cursor_marker_is_never_written_to_the_terminal() { + let output = Arc::new(Mutex::new(String::new())); + let terminal = RecordingTerminal { + output: output.clone(), + }; + let tui = TuiAltScreen::new(Box::new(terminal), true, None); + let editor = Arc::new(Editor::simple()); + editor.set_focused(true); + editor.insert("hello"); + tui.set_layout_root(Some(editor.clone())); + tui.start_readerless(); + + editor.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)); + tui.request_render(false); + + let rendered = output.lock().unwrap().clone(); + assert!(!rendered.contains(CURSOR_MARKER)); + assert!(rendered.contains("hello")); + assert_eq!(editor.get_text(), "hello"); + } + #[test] fn layout_does_not_touch_main_screen_and_stop_clears_it() { let output = Arc::new(Mutex::new(String::new())); diff --git a/deploy/caddy-rpi.override.yml b/deploy/caddy-rpi.override.yml new file mode 100644 index 0000000..4837769 --- /dev/null +++ b/deploy/caddy-rpi.override.yml @@ -0,0 +1,10 @@ +services: + caddy: + volumes: + - ./config/caddy:/etc/caddy:ro + - /var/lib/docker/volumes/app-caddy-data/_data:/data + - /var/lib/docker/volumes/app-caddy-config/_data:/config + - /var/www/openmatrix:/var/www/openmatrix:ro + - /var/www/matrixcode:/var/www/matrixcode:ro + - /var/www/easytier-web:/var/www/easytier-web:ro + - /var/www/rpi:/var/www/rpi:ro diff --git a/deploy/rpi.caddy b/deploy/rpi.caddy new file mode 100644 index 0000000..403e8ca --- /dev/null +++ b/deploy/rpi.caddy @@ -0,0 +1,13 @@ +# rpi.laofu.online - rpi Rust Agent Toolkit static website +rpi.laofu.online { + encode gzip + root * /var/www/rpi + file_server + tls { + issuer acme + } +} + +http://rpi.laofu.online { + redir https://{host}{uri} permanent +} diff --git a/docs/promotion-kit.md b/docs/promotion-kit.md new file mode 100644 index 0000000..9461220 --- /dev/null +++ b/docs/promotion-kit.md @@ -0,0 +1,177 @@ +# rpi 投放素材 + +本文件用于发布 rpi 的开源推广内容。发布前请根据平台规则调整措辞,不要在多个社区原样重复发帖。 + +## 统一信息 + +- 项目:rpi,基于 Pi agent 的 Rust 原生 Agent SDK 和终端 coding agent +- 版本:0.1.6 +- GitHub:https://github.com/bigfish1913/pi-rust +- 官网:https://rpi.laofu.online/ +- 文档:https://rpi.laofu.online/docs.html +- 安装:`cargo install rpi-cli` +- 启动:`rpi -p "hello"` +- 许可证:MIT + +## 中文短文案 + +### V2EX / Linux.do / Rust 中文社区 + +标题: + +> rpi:基于 Pi agent 的 Rust 原生 Agent SDK,支持工具调用、会话和插件 + +正文: + +> rpi 是一个基于 Pi agent 的 Rust 原生实现,采用 library-first、多 crate 的设计,目标是让开发者可以把 Agent runtime 嵌入自己的应用或工作流。 +> +> 目前包含: +> - 异步、流式的 Agent loop +> - Anthropic Messages、OpenAI-compatible 和 faux provider +> - `read`、`write`、`edit`、`bash`、`grep`、`find`、`ls` 等 coding-agent 工具 +> - session、JSONL 持久化、上下文压缩和 prompt templates +> - `rpi-plugin-sdk` 与稳定 ABI 扩展能力 +> - `rpi-cli` 终端编码 Agent +> +> 直接安装: +> +> ```bash +> cargo install rpi-cli +> rpi -p "hello" +> ``` +> +> 项目地址:https://github.com/bigfish1913/pi-rust +> +> 官网和文档:https://rpi.laofu.online/ +> +> 欢迎 Rust、LLM Agent 和开发者工具方向的朋友试用、反馈和贡献。 + +## Hacker News + +标题: + +> Show HN: rpi – A Rust-native Pi agent SDK and terminal coding agent + +正文: + +> rpi is a Rust-native implementation of the Pi agent SDK. It is library-first and split into composable crates for providers, agent runtime, tools, sessions, harnesses, plugins, and a terminal coding-agent CLI. +> +> Highlights: +> - Async, streaming agent runtime +> - Anthropic Messages, OpenAI-compatible chat completions, and a faux provider +> - Built-in coding tools: read, write, edit, bash, grep, find, and ls +> - Durable JSONL sessions, context compaction, hooks, queues, and prompt templates +> - Plugin SDK and stable ABI registration for tools, providers, events, and resources +> +> ```bash +> cargo install rpi-cli +> rpi -p "hello" +> ``` +> +> GitHub: https://github.com/bigfish1913/pi-rust +> +> Website: https://rpi.laofu.online/ +> +> The project is MIT licensed and currently at version 0.1.6. + +## Reddit + +### r/rust + +标题: + +> [Showcase] rpi: a Rust-native Pi agent SDK with composable crates and plugin ABI + +重点:Rust crate 分层、异步流式 runtime、trait 设计、插件 ABI、测试和 faux provider。避免只强调“又一个 AI CLI”。 + +### r/LocalLLaMA / r/LLMDevs + +标题: + +> rpi: a Rust coding-agent toolkit with provider adapters, built-in tools, sessions, and plugins + +重点:Provider 适配、工具循环、会话持久化、OpenAI-compatible endpoint 和不需要 API key 的本地示例。发布前查看各版自荐规则。 + +## GitHub Release + +建议 Release 标题: + +> rpi v0.1.6 — Rust-native Pi agent SDK and CLI + +Release 摘要: + +> rpi v0.1.6 provides the first complete path from provider and agent loop to built-in coding tools, sessions, harness, plugins, and the `rpi` terminal CLI. +> +> Install the CLI with: +> +> ```bash +> cargo install rpi-cli +> ``` +> +> See the documentation at https://rpi.laofu.online/docs.html and the architecture guide at https://github.com/bigfish1913/pi-rust/blob/main/docs/architecture.md. + +## This Week in Rust + +Issue title: + +> Project Submission: rpi — Rust-native Pi agent SDK and terminal coding agent + +Issue body: + +> Hi TWiR team, +> +> I would like to propose rpi for the Project/Crate of the Week. +> +> rpi is a Rust-native implementation of the Pi agent SDK and a terminal coding-agent CLI. It follows a library-first design and splits providers, the async agent loop, coding tools, durable sessions, harnesses, plugins, and the CLI into composable crates. +> +> Highlights: +> - Async, streaming Agent runtime with tool calls, hooks, queues, and cancellation. +> - Anthropic Messages, OpenAI-compatible Chat Completions, and a deterministic faux provider. +> - Built-in `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls` tools. +> - JSONL session persistence, branching, prompt templates, and context compaction. +> - A stable `#[repr(C)]` plugin ABI through `rpi-plugin-sdk`, with a host-side dynamic loader. +> - A terminal CLI that can be installed with `cargo install rpi-cli`. +> +> Quick start: +> +> ```bash +> cargo install rpi-cli +> rpi -p "hello" +> ``` +> +> Links: +> - Repository: https://github.com/bigfish1913/pi-rust +> - Website: https://rpi.laofu.online/ +> - Crates.io: https://crates.io/crates/rpi-cli +> - Docs.rs: https://docs.rs/rpi-cli +> - Plugin SDK: https://crates.io/crates/rpi-plugin-sdk + +## Awesome Rust PR + +Suggested list entry for an appropriate AI / agent section: + +> - [rpi](https://github.com/bigfish1913/pi-rust) - Rust-native Pi agent SDK and terminal coding-agent CLI with composable providers, tools, sessions, and a stable plugin ABI. + +Use the list maintainer's preferred category and contribution format. Submit the +entry as a focused PR rather than opening multiple issues. + +## X / Bluesky / LinkedIn + +> Introducing rpi: a Rust-native implementation of the Pi agent SDK. +> +> Build composable coding agents with async streaming, provider adapters, built-in tools, durable sessions, and a plugin ABI. +> +> Install: `cargo install rpi-cli` +> +> GitHub: https://github.com/bigfish1913/pi-rust +> Website: https://rpi.laofu.online/ + +## 发布检查清单 + +- 确认链接使用 `https://github.com/bigfish1913/pi-rust`,不要使用旧仓库地址。 +- 确认版本号为 `0.1.6`,不要写成 `0.1.x`。 +- 确认安装命令是 `cargo install rpi-cli`。 +- 每个平台使用一张最相关的截图,正文中说明截图展示的是 CLI 启动或工作状态。 +- Hacker News 和 Reddit 使用英文;中文社区使用中文,并按版规选择分类。 +- 发帖后优先回复安装、Provider 配置和插件扩展问题,不要连续重复推送。 +- GitHub Issue、论坛帖子和社交平台公开提交前,确认标题、链接、截图和项目状态无误。 diff --git a/docs/release-v0.1.6.md b/docs/release-v0.1.6.md new file mode 100644 index 0000000..68f3a19 --- /dev/null +++ b/docs/release-v0.1.6.md @@ -0,0 +1,26 @@ +# rpi v0.1.6 + +rpi v0.1.6 is a Rust-native implementation of the Pi agent SDK, with a library-first multi-crate workspace and a terminal coding-agent CLI. + +## Highlights + +- Async, streaming agent runtime +- Anthropic Messages, OpenAI-compatible Chat Completions, and faux provider +- Built-in `read`, `write`, `edit`, `bash`, `grep`, `find`, and `ls` coding tools +- JSONL sessions, context compaction, hooks, queues, and prompt templates +- `rpi-plugin-sdk` and stable ABI extension points + +## Install + +```bash +cargo install rpi-cli +rpi -p "hello" +``` + +## Links + +- Documentation: https://rpi.laofu.online/docs.html +- Architecture: https://github.com/bigfish1913/pi-rust/blob/main/docs/architecture.md +- Website: https://rpi.laofu.online/ + +MIT License. diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..0a1d9a5 --- /dev/null +++ b/website/README.md @@ -0,0 +1,11 @@ +# rpi 官网 + +这是一个零依赖的静态官网原型。首页数据来自 `data/site.json`,Packages 目录数据来自 `data/packages.json`,Documentation 数据来自 `data/docs.json`;页面逻辑分别在 `app.js`、`packages.js` 和 `docs.js`,共享 `styles.css`。 + +在仓库根目录启动: + +```powershell +python -m http.server 4173 --directory website +``` + +然后打开 。文档页支持“使用文档 / SDK 文档”切换和章节搜索。后续把页面里的 `fetch(...)` 替换为 API 请求即可接入真实后台。 diff --git a/website/app.js b/website/app.js new file mode 100644 index 0000000..d0c896b --- /dev/null +++ b/website/app.js @@ -0,0 +1,85 @@ +const state = { data: null, activeExample: 0 }; +const $ = (selector, scope = document) => scope.querySelector(selector); +const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]; + +function getPath(source, path) { return path.split('.').reduce((value, key) => value?.[key], source); } + +function renderFeatures(features) { + $('[data-features]').innerHTML = features.map((feature) => ` +

${feature.number}${feature.tag}

${feature.title}

${feature.description}

+ `).join(''); +} + +function renderArchitecture(layers) { + $('[data-architecture]').innerHTML = layers.map((layer, index) => ` +
0${index + 1}
${layer.name}
${layer.label}

${layer.description}

${index < layers.length - 1 ? '' : ''}
+ `).join(''); +} + +function renderExamples(examples) { + const tabs = $('[data-example-tabs]'); + tabs.innerHTML = examples.map((example, index) => ` + + `).join(''); + $$('.code-tab', tabs).forEach((tab) => tab.addEventListener('click', () => { state.activeExample = Number(tab.dataset.exampleIndex); updateExample(examples); })); + updateExample(examples); +} + +function updateExample(examples) { + const example = examples[state.activeExample]; + $$('.code-tab').forEach((tab, index) => { const active = index === state.activeExample; tab.classList.toggle('is-active', active); tab.setAttribute('aria-selected', String(active)); }); + $('[data-code-file]').textContent = example.file; + $('[data-code-note]').textContent = example.note; + $('[data-code-content]').textContent = example.code; +} + +function renderReleases(releases) { + $('[data-releases]').innerHTML = releases.map((release) => ` +
${release.version}${release.status}

${release.title}

${release.description}

+ `).join(''); +} + +function bindCopy(buttonSelector, getText, successMessage) { + $(buttonSelector)?.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(getText()); showToast(successMessage); } catch { showToast('复制失败,请手动选择文本'); } + }); +} + +function showToast(message) { + const toast = $('[data-toast]'); toast.textContent = message; toast.classList.add('is-visible'); + window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => toast.classList.remove('is-visible'), 2200); +} + +function bindMenu() { + const toggle = $('[data-menu-toggle]'); const menu = $('[data-mobile-menu]'); + toggle.addEventListener('click', () => { const open = menu.classList.toggle('is-open'); toggle.setAttribute('aria-expanded', String(open)); }); + $$('a', menu).forEach((link) => link.addEventListener('click', () => { menu.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); })); +} + +function bindScrollEffects() { + const header = $('[data-sticky-header]'); const marker = document.createElement('div'); marker.className = 'scroll-marker'; document.body.prepend(marker); + const observer = new IntersectionObserver(([entry]) => header.classList.toggle('is-scrolled', !entry.isIntersecting), { threshold: 0 }); observer.observe(marker); +} + +async function init() { + const response = await fetch('./data/site.json'); state.data = await response.json(); + $$('[data-content]').forEach((element) => { + const value = getPath(state.data, element.dataset.content); + if (!value) return; + if (element.dataset.content === 'hero.title') { + const [lead] = value.split('你自己的工具。'); + element.innerHTML = `${lead}
你自己的工具。`; + return; + } + element.textContent = value; + }); + $$('[data-stat]').forEach((element) => { const value = getPath(state.data, element.dataset.stat); if (value) element.textContent = value; }); + renderFeatures(state.data.features); renderArchitecture(state.data.architecture); renderExamples(state.data.examples); renderReleases(state.data.releases); + bindCopy('[data-copy-code]', () => state.data.examples[state.activeExample].code, '代码已复制'); + $$('[data-copy-command]').forEach((button) => button.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(button.previousElementSibling.textContent); showToast('命令已复制'); } catch { showToast('复制失败,请手动选择文本'); } + })); + bindMenu(); bindScrollEffects(); +} + +init().catch((error) => { console.error(error); showToast('数据加载失败,请通过本地开发服务器打开网站'); }); diff --git a/website/data/docs.json b/website/data/docs.json new file mode 100644 index 0000000..b713f01 --- /dev/null +++ b/website/data/docs.json @@ -0,0 +1,188 @@ +{ + "title": "Documentation", + "intro": "使用文档和 SDK 文档,帮助你从第一个 faux agent 开始,逐步接入工具、会话和插件。", + "modes": { + "guide": { + "label": "使用文档", + "eyebrow": "RPI / USER GUIDE", + "description": "面向第一次使用 rpi 的开发者:从安装、运行到把 Agent 接进自己的应用。", + "sections": [ + { + "id": "quickstart", + "kicker": "01 / FIRST RUN", + "title": "快速开始", + "summary": "先用 faux provider 跑通完整闭环,不需要 API key,也不需要网络。", + "body": [ + "rpi 是 library-first 的 Rust Agent toolkit。建议先运行仓库里的 minimal 示例,再根据需要组合 rpi-ai、rpi-agent 和 rpi-tools。", + "faux provider 会返回确定性的本地响应,适合学习事件流、写测试和验证工具循环。" + ], + "code": {"language": "shell", "file": "examples/minimal/src/main.rs", "content": "cargo run -p minimal"}, + "links": [{"label": "README / How you build an agent", "url": "https://github.com/bigfish1913/pi-rust#how-you-build-an-agent"}] + }, + { + "id": "install", + "kicker": "02 / INSTALL", + "title": "安装与运行", + "summary": "Rust 1.78+、Cargo 和 Tokio 是开始使用的最低条件;也可以直接安装 rpi-cli。", + "body": [ + "只想直接使用 CLI 时执行 cargo install rpi-cli;想研究 Agent loop,再克隆仓库运行 minimal 示例。Cargo workspace 会按依赖顺序编译所有 crate。", + "真实 Provider 的凭据可以通过命令行参数、~/.rpi/auth.json、models.json 或环境变量提供。不要把密钥提交进仓库。" + ], + "code": {"language": "shell", "file": "README.md", "content": "# 直接安装 CLI\ncargo install rpi-cli\nrpi -p \"hello\"\n\n# 从源码运行示例\ngit clone https://github.com/bigfish1913/pi-rust.git\ncd pi-rust\ncargo run -p minimal"}, + "links": [{"label": "配置说明", "url": "https://github.com/bigfish1913/pi-rust#configuration"}] + }, + { + "id": "first-agent", + "kicker": "03 / AGENT", + "title": "第一个 Agent", + "summary": "用 AgentBuilder 声明模型、系统提示词和工具,然后订阅事件并发送 prompt。", + "body": [ + "AgentBuilder 把运行时依赖集中在构建阶段,Agent 本身负责维护消息、事件和待处理 prompt。事件订阅应在调用 prompt 前建立。", + "生产环境通常会把事件映射到 UI 或 telemetry;测试环境可以直接收集 AgentEnd 事件做断言。" + ], + "code": {"language": "rust", "file": "README.md", "content": "use rpi_agent::{Agent, AgentEvent};\nuse rpi_ai::providers::faux::faux_provider;\n\nlet model = faux_provider().model(\"echo\");\nlet mut agent = Agent::builder(model)\n .system_prompt(\"You are a helpful assistant.\")\n .build();\n\nlet mut events = agent.subscribe();\nagent.prompt(\"Hello!\").await?;\nwhile let Some(event) = events.recv().await {\n if matches!(event, AgentEvent::AgentEnd { .. }) { break; }\n}"}, + "links": [{"label": "rpi-agent API", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/pi-agent"}] + }, + { + "id": "tools", + "kicker": "04 / TOOLS", + "title": "接入工具", + "summary": "把 AgentTool 加入 builder;内置工具通过 ExecutionEnv 隔离文件系统和命令执行。", + "body": [ + "rpi-tools 提供 read、write、edit、bash、grep、find、ls 七个基础工具。读操作可以直接执行,写操作通过 MutationQueue 串行化。", + "在测试中使用 InMemoryExecutionEnv 可以避免触碰真实文件系统;需要自定义工具时,实现 rpi-agent 的 AgentTool trait。" + ], + "code": {"language": "rust", "file": "crates/pi-tools/src/lib.rs", "content": "use rpi_tools::{create_read_tool, InMemoryExecutionEnv};\n\nlet env = InMemoryExecutionEnv::default();\nlet read = create_read_tool(env.clone());\nlet mut agent = Agent::builder(model)\n .tool(read)\n .build();"}, + "links": [{"label": "内置工具与 ExecutionEnv", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/pi-tools"}] + }, + { + "id": "providers", + "kicker": "05 / PROVIDERS", + "title": "配置 Provider", + "summary": "统一的 Model / Provider 抽象让 Anthropic、OpenAI-compatible 和 faux provider 可以互换。", + "body": [ + "默认模型会从已认证的模型目录中选择。自定义网关写入 ~/.rpi/agent/models.json,并通过 provider/model 的形式传给 CLI。", + "Anthropic 支持 ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN;OpenAI-compatible endpoint 使用 openai-completions API 配置。" + ], + "code": {"language": "json", "file": "~/.rpi/agent/models.json", "content": "{\n \"providers\": {\n \"gateway\": {\n \"api\": \"anthropic-messages\",\n \"baseUrl\": \"https://gateway.example.com\",\n \"apiKey\": \"sk-gateway-secret\",\n \"models\": [{ \"id\": \"custom-claude\", \"name\": \"Custom Claude\" }]\n }\n }\n}"}, + "links": [{"label": "Provider 配置", "url": "https://github.com/bigfish1913/pi-rust#configuration"}] + }, + { + "id": "sessions", + "kicker": "06 / PERSISTENCE", + "title": "Session 和 Harness", + "summary": "需要持久化、压缩和上下文文件时,把 Agent 交给 AgentHarness。", + "body": [ + "rpi-harness 在 Agent 之上组合 session tree、JSONL 持久化、compaction、skills、prompt templates 和 context files。核心 Agent 仍然可以独立使用。", + "建议把 session id 作为产品层的稳定标识,使用 JSONL backend 记录可回放的事件和消息。" + ], + "code": {"language": "rust", "file": "crates/pi-harness/src/lib.rs", "content": "use rpi_harness::AgentHarness;\n\nlet harness = AgentHarness::builder(agent)\n .session_dir(\".rpi/sessions\")\n .build()\n .await?;\nharness.prompt(\"Summarize this file\").await?;"}, + "links": [{"label": "Harness 模块", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/pi-harness"}] + }, + { + "id": "extensions", + "kicker": "07 / EXTENSIONS", + "title": "插件与扩展", + "summary": "用稳定 ABI 的 cdylib 插件注册工具、Provider、命令和资源。", + "body": [ + "插件 crate 依赖 rpi-plugin-sdk,通过 rpi_plugin_register 导出入口接收 PluginApiVt,并向宿主注册工具、Provider、事件和资源处理器。宿主的 rpi-extensions 负责发现、加载、注册和生命周期管理。", + "把跨边界的数据限制在 SDK 定义的可序列化类型内;插件不应直接依赖宿主内部模块。" + ], + "code": {"language": "rust", "file": "examples/plugin-stub/src/lib.rs", "content": "use rpi_plugin_sdk::{register_entrypoint, PluginApiVt};\n\n#[no_mangle]\npub extern \"C\" fn rpi_plugin_register(\n api: *const PluginApiVt,\n abi_version: u32,\n) -> i32 {\n register_entrypoint(api, abi_version, |api| {\n // api.register_tool / register_provider / register_event_handler\n // are the stable ABI registration slots.\n let _ = api;\n 0\n })\n}"}, + "links": [{"label": "插件 stub", "url": "https://github.com/bigfish1913/pi-rust/tree/main/examples/plugin-stub"}, {"label": "插件加载器", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/rpi-extensions"}] + }, + { + "id": "testing", + "kicker": "08 / TESTING", + "title": "测试与 faux provider", + "summary": "让模型和执行环境都可替换,测试 Agent 行为而不是测试网络。", + "body": [ + "faux provider 适合验证消息顺序、工具调用和 AgentEnd;InMemoryExecutionEnv 适合验证读写工具。两者组合可以构造完全离线的集成测试。", + "对 Provider 适配层再补充少量 HTTP fixture 测试,覆盖流式事件解析、错误映射和工具参数校验。" + ], + "code": {"language": "rust", "file": "crates/pi-ai/src/lib.rs", "content": "let model = faux_provider().model(\"echo\");\nlet mut agent = Agent::builder(model)\n .tool(create_read_tool(InMemoryExecutionEnv::default()))\n .build();\nagent.prompt(\"read README.md\").await?;"}, + "links": [{"label": "faux provider 源码", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/pi-ai"}] + } + ] + }, + "sdk": { + "label": "SDK 文档", + "eyebrow": "RPI / SDK REFERENCE", + "description": "按 crate 和核心 trait 查找 API 边界,适合实现自定义 Provider、工具、Harness 或插件。", + "sections": [ + { + "id": "crate-overview", + "kicker": "01 / WORKSPACE", + "title": "Crate 总览", + "summary": "六层 workspace 保持单向依赖:从 telemetry 和 AI 类型一路组合到 CLI。", + "body": ["发布名使用 rpi- 前缀,磁盘目录仍保留 pi-* 历史路径。核心 crate 可以独立嵌入,不必依赖 CLI。", "典型依赖方向是 rpi-telemetry → rpi-ai → rpi-agent → rpi-tools → rpi-harness → rpi-cli。"], + "code": {"language": "text", "file": "README.md", "content": "rpi-telemetry telemetry contracts\nrpi-ai provider types + streaming\nrpi-agent runtime loop + AgentTool\nrpi-tools built-in tools + ExecutionEnv\nrpi-harness sessions + compaction\nrpi-cli terminal application"}, + "links": [{"label": "Workspace README", "url": "https://github.com/bigfish1913/pi-rust#crates-published-as-rpi-"}] + }, + { + "id": "rpi-ai", + "kicker": "02 / RPI-AI", + "title": "rpi-ai", + "summary": "统一 Model、Provider、Message、Context 和流式 AssistantMessageEvent。", + "body": ["Provider 负责把统一请求转换成具体模型 API,并将响应转换回 SDK 事件。validate_tool_arguments 在调用前校验 JSON 参数。", "实现新 Provider 时优先复用现有消息类型和流式事件,不要在上层泄漏供应商专用结构。"], + "code": {"language": "rust", "file": "crates/pi-ai/src/provider.rs", "content": "#[async_trait]\npub trait Provider: Send + Sync {\n fn id(&self) -> &str;\n fn models(&self) -> &[Model];\n async fn stream_simple(\n &self,\n model: &Model,\n ctx: &Context,\n opts: &SimpleStreamOptions,\n ) -> AssistantMessageEventStream;\n}"}, + "links": [{"label": "rpi-ai exports", "url": "https://github.com/bigfish1913/pi-rust/blob/main/crates/pi-ai/src/lib.rs"}] + }, + { + "id": "rpi-agent", + "kicker": "03 / RPI-AGENT", + "title": "rpi-agent", + "summary": "Agent、AgentBuilder、AgentTool、AgentEvent 和 run_agent_loop 构成运行时核心。", + "body": ["AgentBuilder 负责组装模型、系统提示词、工具和 hooks;run_agent_loop 负责在消息、工具调用和 Provider 流之间推进状态。", "PendingMessageQueue 允许在运行过程中排队新的用户消息,订阅 AgentEvent 可以驱动界面和 telemetry。"], + "code": {"language": "rust", "file": "crates/pi-agent/src/agent_tool.rs", "content": "#[async_trait]\npub trait AgentTool: Send + Sync {\n fn schema(&self) -> &Tool;\n fn label(&self) -> &str;\n async fn execute(\n &self,\n tool_call_id: &str,\n params: serde_json::Value,\n signal: CancellationToken,\n on_update: Arc,\n ) -> Result;\n}"}, + "links": [{"label": "rpi-agent exports", "url": "https://github.com/bigfish1913/pi-rust/blob/main/crates/pi-agent/src/lib.rs"}] + }, + { + "id": "rpi-tools", + "kicker": "04 / RPI-TOOLS", + "title": "rpi-tools", + "summary": "ExecutionEnv 把工具的文件、进程和路径操作隔离出来。", + "body": ["OsExecutionEnv 连接真实操作系统,InMemoryExecutionEnv 为测试提供内存实现。create_*_tool 函数返回可直接注册的 AgentTool。", "将高风险写操作放在 MutationQueue 中串行执行,并在产品层增加确认策略。"], + "code": {"language": "rust", "file": "crates/pi-tools/src/lib.rs", "content": "pub trait ExecutionEnv: Send + Sync {\n async fn read_file(&self, path: &Path) -> Result;\n async fn write_file(&self, path: &Path, content: &str) -> Result<()>;\n async fn run_command(&self, command: &str) -> Result;\n}\n\npub fn create_grep_tool(env: Arc) -> impl AgentTool { /* ... */ }"}, + "links": [{"label": "ExecutionEnv API", "url": "https://github.com/bigfish1913/pi-rust/blob/main/crates/pi-tools/src/lib.rs"}] + }, + { + "id": "rpi-harness", + "kicker": "05 / RPI-HARNESS", + "title": "rpi-harness", + "summary": "在 Agent 之上提供 session、compaction、skills、模板和上下文文件。", + "body": ["Harness 是产品集成层:它知道如何恢复 session、裁剪上下文、加载 skills,并把系统提示词拼成一次运行所需的 Context。", "如果你只需要一次性的 Agent 调用,可以直接使用 rpi-agent;需要持久化时再引入 Harness。"], + "code": {"language": "rust", "file": "crates/pi-harness/src/lib.rs", "content": "pub mod agent_harness;\npub mod session;\npub mod compaction;\npub mod skills;\npub mod prompt_templates;\npub mod context_files;\npub mod events;\npub mod system_prompt;"}, + "links": [{"label": "Harness modules", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/pi-harness"}] + }, + { + "id": "lifecycle", + "kicker": "06 / LIFECYCLE", + "title": "Agent 生命周期", + "summary": "构建、订阅、prompt、工具循环、结束事件是最小生命周期。", + "body": ["创建 Agent 后先订阅事件,再发送 prompt。Provider 产生消息增量,工具调用完成后会回到循环,最终以 AgentEnd 收束。", "hooks 用于在生命周期节点插入日志、策略或 UI 状态同步,但不应改变核心消息契约。"], + "code": {"language": "text", "file": "crates/pi-agent/src/lib.rs", "content": "AgentBuilder\n -> Agent::build\n -> subscribe()\n -> prompt(message)\n -> Provider stream\n -> AgentTool execution\n -> AgentEvent::AgentEnd"}, + "links": [{"label": "Agent loop", "url": "https://github.com/bigfish1913/pi-rust/blob/main/crates/pi-agent/src/lib.rs"}] + }, + { + "id": "events", + "kicker": "07 / EVENTS", + "title": "Provider 与 AgentEvent", + "summary": "用统一事件流连接模型、运行时和前端,不把供应商协议带到 UI。", + "body": ["AssistantMessageEventStream 表达文本增量、工具调用和结束原因;AgentEvent 在此基础上加入工具执行和运行生命周期。", "UI 应按事件增量渲染,持久化层则在结束时保存完整消息和 usage。"], + "code": {"language": "rust", "file": "crates/pi-ai/src/lib.rs", "content": "pub enum AssistantMessageEvent {\n Start,\n TextDelta(String),\n ToolCall { name: String, arguments: Value },\n Done { stop_reason: StopReason },\n Error(String),\n}"}, + "links": [{"label": "AI event types", "url": "https://github.com/bigfish1913/pi-rust/blob/main/crates/pi-ai/src/lib.rs"}] + }, + { + "id": "plugin-abi", + "kicker": "08 / PLUGIN ABI", + "title": "插件 ABI", + "summary": "rpi-plugin-sdk 是插件和宿主之间唯一应依赖的边界。", + "body": ["插件以 cdylib 编译,由 rpi-extensions loader 发现并加载。registry 负责保存 API 注册的工具、Provider、事件和资源处理器,宿主控制卸载与错误隔离。", "ABI 变更应提升版本并同时更新 plugin-stub 与 smoke test;不要让插件直接链接 rpi-cli 私有模块。"], + "code": {"language": "toml", "file": "crates/rpi-plugin-sdk/Cargo.toml", "content": "[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nrpi-plugin-sdk = \"0.1\""}, + "links": [{"label": "SDK crate", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/rpi-plugin-sdk"}, {"label": "ABI smoke test", "url": "https://github.com/bigfish1913/pi-rust/tree/main/crates/rpi-extensions/tests"}] + } + ] + } + } +} diff --git a/website/data/packages.json b/website/data/packages.json new file mode 100644 index 0000000..0179fe5 --- /dev/null +++ b/website/data/packages.json @@ -0,0 +1,74 @@ +[ + { + "name": "rpi-cli", + "type": "crate", + "age": 1, + "ageLabel": "v0.1.6 · current", + "description": "终端 coding-agent CLI,提供 rpi 命令、交互式 TUI、配置和插件加载。", + "author": "rpi / crates.io", + "downloads": 105, + "downloadsLabel": "105", + "install": "cargo install rpi-cli", + "repository": "https://crates.io/crates/rpi-cli" + }, + { + "name": "rpi-agent", + "type": "crate", + "age": 2, + "ageLabel": "v0.1.6 · current", + "description": "异步 Agent runtime 和 loop,包含 AgentTool、事件、hooks 与消息队列。", + "author": "rpi / crates.io", + "downloads": 165, + "downloadsLabel": "165", + "install": "cargo add rpi-agent", + "repository": "https://crates.io/crates/rpi-agent" + }, + { + "name": "rpi-tools", + "type": "crate", + "age": 3, + "ageLabel": "v0.1.6 · current", + "description": "开箱即用的 read、write、edit、bash、grep、find、ls 工具和 ExecutionEnv。", + "author": "rpi / crates.io", + "downloads": 138, + "downloadsLabel": "138", + "install": "cargo add rpi-tools", + "repository": "https://crates.io/crates/rpi-tools" + }, + { + "name": "rpi-plugin-sdk", + "type": "extension", + "age": 4, + "ageLabel": "v0.1.6 · ABI v1", + "description": "Rust cdylib 插件的稳定 #[repr(C)] ABI 合约,插件只依赖这一层边界。", + "author": "rpi / crates.io", + "downloads": 37, + "downloadsLabel": "37", + "install": "cargo add rpi-plugin-sdk", + "repository": "https://crates.io/crates/rpi-plugin-sdk" + }, + { + "name": "rpi-extensions", + "type": "extension", + "age": 5, + "ageLabel": "v0.1.6 · loader", + "description": "宿主侧动态插件加载器,将 ABI 插件适配为 AgentTool 并管理生命周期。", + "author": "rpi / crates.io", + "downloads": 38, + "downloadsLabel": "38", + "install": "cargo add rpi-extensions", + "repository": "https://crates.io/crates/rpi-extensions" + }, + { + "name": "rpi-harness", + "type": "crate", + "age": 6, + "ageLabel": "v0.1.6 · sessions", + "description": "Session 树、JSONL 持久化、上下文压缩和 AgentHarness run loop。", + "author": "rpi / crates.io", + "downloads": 126, + "downloadsLabel": "126", + "install": "cargo add rpi-harness", + "repository": "https://crates.io/crates/rpi-harness" + } +] diff --git a/website/data/site.json b/website/data/site.json new file mode 100644 index 0000000..abe8d48 --- /dev/null +++ b/website/data/site.json @@ -0,0 +1,47 @@ +{ + "hero": { + "eyebrow": "RUST AGENT TOOLKIT", + "title": "把 Agent 做成你自己的工具。", + "description": "rpi 是一个 library-first 的 Rust SDK。用清晰的分层、可替换的模型 Provider 和真正可测试的工具循环,构建属于你的 coding agent。" + }, + "stats": [ + { "value": "6", "label": "个可组合 crate" }, + { "value": "2", "label": "种内置 Provider" }, + { "value": "7", "label": "个开箱即用工具" }, + { "value": "0", "label": "网络依赖的测试" } + ], + "features": [ + { "number": "01", "title": "Library-first", "description": "核心能力不依赖 CLI。只引入你需要的 crate,从一个 Agent loop 开始。", "tag": "可组合" }, + { "number": "02", "title": "Provider agnostic", "description": "Anthropic、OpenAI-compatible 或你自己的服务,都落在同一个 Provider 边界后面。", "tag": "可替换" }, + { "number": "03", "title": "Tool loop ready", "description": "工具参数校验、并行执行、流式事件、取消和 follow-up 都是循环的一部分。", "tag": "可观测" }, + { "number": "04", "title": "Durable by choice", "description": "需要时再接入 Session、JSONL、分支和上下文压缩,不为简单实验提前付复杂度。", "tag": "可持久化" } + ], + "architecture": [ + { "name": "rpi-ai", "label": "Models + streaming", "description": "统一消息类型、Provider 和流式事件", "tone": "orange" }, + { "name": "rpi-agent", "label": "Agent loop", "description": "工具调用、事件、Hook、steering", "tone": "mint" }, + { "name": "rpi-tools", "label": "Built-in tools", "description": "read / write / edit / bash / grep", "tone": "blue" }, + { "name": "rpi-harness", "label": "Sessions", "description": "会话树、JSONL、compaction", "tone": "violet" }, + { "name": "rpi-cli", "label": "Your interface", "description": "CLI、TUI、配置和插件", "tone": "cream" } + ], + "examples": [ + { + "id": "minimal", + "label": "Minimal agent", + "file": "examples/minimal/src/main.rs", + "note": "确定性的本地 provider,不需要 API key。", + "code": "use rpi_agent::AgentBuilder;\nuse rpi_ai::providers::faux::{FauxProvider, FauxScript};\n\n#[tokio::main]\nasync fn main() {\n let script = FauxScript::new()\n .with_text(\"Hello from rpi!\");\n let provider = FauxProvider::new(script);\n let model = provider.default_model().clone();\n\n let agent = AgentBuilder::new()\n .model(model)\n .stream_fn(make_stream_fn(provider))\n .build()\n .expect(\"agent builds\");\n\n agent.prompt(\"hi\").await.expect(\"prompt completes\");\n}\n" + }, + { + "id": "tools", + "label": "Tool loop", + "file": "examples/tools/src/main.rs", + "note": "把内置 write 和 bash 工具接进真实文件系统。", + "code": "let env = Arc::new(OsExecutionEnv::with_cwd(tmp));\nlet ctx = ExecutionToolContext::new(\n env.clone() as Arc,\n Some(env as Arc),\n);\n\nlet write = create_write_tool(&ctx);\nlet bash = create_bash_tool(&ctx, None);\n\nlet agent = AgentBuilder::new()\n .model(model)\n .stream_fn(make_stream_fn(provider))\n .tools(vec![write, bash])\n .build()?;\n\nagent.prompt(\"Write hello to out.txt\").await?;\n" + } + ], + "releases": [ + { "version": "0.1.6", "status": "current", "title": "Core stable", "description": "Provider、Agent loop、工具、Harness 和 CLI 组成第一条完整路径。" }, + { "version": "next", "status": "building", "title": "More surfaces", "description": "扩展 package manifest、更多 Provider 和更完整的插件资源协议。" }, + { "version": "future", "status": "exploring", "title": "Your ideas", "description": "把 rpi 接进你的产品、工作流和自己的运行环境。" } + ] +} diff --git a/website/docs.html b/website/docs.html new file mode 100644 index 0000000..ad3f7de --- /dev/null +++ b/website/docs.html @@ -0,0 +1,44 @@ + + + + + + + Documentation · rpi + + + + + + +
+
+ rpi + + +
+ + +
+
+
RPI / KNOWLEDGE BASE
+

Documentation

+

使用文档和 SDK 文档,帮助你从第一个 faux agent 开始,逐步接入工具、会话和插件。

+
+
+
+
+ +
+
+ +

+
+
+
+ +
+
+ + + diff --git a/website/docs.js b/website/docs.js new file mode 100644 index 0000000..bac7dd3 --- /dev/null +++ b/website/docs.js @@ -0,0 +1,64 @@ +const $ = (selector, scope = document) => scope.querySelector(selector); +const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]; +const state = { data: null, mode: 'guide', query: '' }; + +function showToast(message) { + const toast = $('[data-toast]'); toast.textContent = message; toast.classList.add('is-visible'); + clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('is-visible'), 2200); +} + +function bindMenu() { + const toggle = $('[data-menu-toggle]'); const menu = $('[data-mobile-menu]'); + toggle.addEventListener('click', () => { const open = menu.classList.toggle('is-open'); toggle.setAttribute('aria-expanded', String(open)); }); + $$('a', menu).forEach((link) => link.addEventListener('click', () => { menu.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); })); +} + +function bindScrollEffects() { + const header = $('[data-sticky-header]'); const marker = document.createElement('div'); marker.className = 'scroll-marker'; document.body.prepend(marker); + new IntersectionObserver(([entry]) => header.classList.toggle('is-scrolled', !entry.isIntersecting), { threshold: 0 }).observe(marker); +} + +function currentMode() { return state.data.modes[state.mode]; } + +function matches(section) { + const query = state.query.trim().toLowerCase(); + if (!query) return true; + return `${section.title} ${section.summary} ${section.kicker} ${section.body.join(' ')} ${section.code?.file || ''}`.toLowerCase().includes(query); +} + +function renderModes() { + $('[data-docs-modes]').innerHTML = Object.entries(state.data.modes).map(([key, mode], index) => ``).join(''); + $$('[data-docs-mode]').forEach((button) => button.addEventListener('click', () => { state.mode = button.dataset.docsMode; state.query = ''; $('[data-docs-search]').value = ''; render(); })); +} + +function renderNav(sections) { + const nav = $('[data-docs-nav]'); + nav.innerHTML = sections.length ? sections.map((section) => `${section.kicker.split(' / ')[0]}${section.title}`).join('') : ''; + $$('a', nav).forEach((link) => link.addEventListener('click', (event) => { event.preventDefault(); document.getElementById(link.hash.slice(1))?.scrollIntoView({ behavior: 'smooth', block: 'start' }); history.replaceState(null, '', link.hash); })); +} + +function sectionMarkup(section) { + const links = (section.links || []).map((link) => `${link.label} ↗`).join(''); + const code = section.code ? `
${section.code.file}
${section.code.content}
` : ''; + return `
${section.kicker}

${section.title}

${section.summary}

${section.body.map((paragraph) => `

${paragraph}

`).join('')}${code}
`; +} + +function bindCodeCopy() { + $$('[data-copy-docs-code]').forEach((button) => button.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(button.closest('.docs-code').querySelector('code').textContent); showToast('代码已复制'); } catch { showToast('复制失败,请手动选择文本'); } + })); +} + +function render() { + const mode = currentMode(); const sections = mode.sections.filter(matches); + $('[data-docs-eyebrow]').textContent = mode.eyebrow; $('[data-docs-description]').textContent = mode.description; + renderModes(); renderNav(sections); $('[data-docs-sections]').innerHTML = sections.map(sectionMarkup).join(''); $('[data-docs-empty]').hidden = sections.length > 0; bindCodeCopy(); +} + +async function init() { + const response = await fetch('./data/docs.json?v=2'); state.data = await response.json(); + bindMenu(); bindScrollEffects(); render(); + $('[data-docs-search]').addEventListener('input', (event) => { state.query = event.target.value; render(); }); +} + +init().catch((error) => { console.error(error); showToast('文档数据加载失败,请通过本地开发服务器打开网站'); }); diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..e8e992e --- /dev/null +++ b/website/index.html @@ -0,0 +1,62 @@ + + + + + + + rpi — Rust Agent Toolkit + + + + + + +
+
+ rpi + + +
+ + +
+
+
+
RUST AGENT TOOLKIT
+

把 Agent 做成
你自己的工具。

+

rpi 是一个 library-first 的 Rust SDK。用清晰的分层、可替换的模型 Provider 和真正可测试的工具循环,构建属于你的 coding agent。

+ +
MIT LicenseRust 1.78+无网络也能测试
+
+
+
+
+
rpi / agent-session LIVE
+
+
$ cargo run -p minimal
+
01Agent::prompt("hi")
+
02stream.start · faux-1
+
03↳ assistant_message
04Hello from the faux provider!
+
05message.end · 18ms
+
AgentEnd observed. 2 messages after run
+
+ +
+
STREAMING18ms
COMPOSABLE6 crates
+
+
+
6个可组合 crate
2种内置 Provider
7个开箱即用工具
0网络依赖的测试
+ +
01 / WHY RPI

从一条 prompt,
长出完整系统。

Agent 不应该被锁在一个 CLI 里。rpi 把模型、循环、工具、会话和终端体验拆成可以独立替换的 Rust 层,让你从最小实验开始,逐步长成自己的产品。

+
02 / ARCHITECTURE

每一层都有自己的边界。

从 Provider 到 CLI,单向依赖让核心库保持轻盈,也让你的扩展有明确的落点。

+
03 / START SMALL

先跑起来,
再加复杂度。

两个示例覆盖最重要的学习路径:先用 faux provider 跑通 Agent,再把真实文件系统和工具接进循环。

examples/minimal/src/main.rs
确定性的本地 provider,不需要 API key。浏览 examples ↗
+
04 / EXTEND THE LOOP

把你的能力
插进 Agent。

Rust cdylib 插件通过稳定 ABI 接入 Tool、Provider、Slash Command、事件和资源发现。宿主保留生命周期,插件只关注自己的能力。

了解插件系统
+
05 / RELEASES

小步发布,持续靠近。

核心能力优先稳定下来,再把更多 Provider、插件生态和 CLI 体验交给社区。

+
06 / GET STARTED

你的 Agent,
从这里开始。

想直接使用 CLI 就安装 rpi-cli;想研究 Agent loop,再拉源码运行 faux provider。

直接安装 CLI
$cargo install rpi-cli
从源码运行示例
$git clone https://github.com/bigfish1913/pi-rust.git && cd pi-rust && cargo run -p minimal
+
+ +
+
+ + + diff --git a/website/packages.html b/website/packages.html new file mode 100644 index 0000000..ae1cdff --- /dev/null +++ b/website/packages.html @@ -0,0 +1,44 @@ + + + + + + + Packages · rpi + + + + + + +
+
+ rpi + + +
+ + +
+
+
RPI / PACKAGE CATALOG
+

把 Agent 的能力,
装进你的工作流。

+

扩展、skills、providers 和 prompt templates 的静态目录。先从这里发现组合方式,再用 Cargo 或 Git 接入自己的 rpi 项目。

+
$cargo add rpi-tools
+
+ +
+
DISCOVER / 0 PACKAGES

最近值得试试的东西。

这些条目先用静态 JSON 代替后端数据。以后可以直接接 crates.io、GitHub 或你自己的 registry。

+
+
+
0 个结果
+
+ +
+
+ +
+
+ + + diff --git a/website/packages.js b/website/packages.js new file mode 100644 index 0000000..ce22b02 --- /dev/null +++ b/website/packages.js @@ -0,0 +1,62 @@ +const $ = (selector, scope = document) => scope.querySelector(selector); +const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]; +const state = { packages: [], query: '', type: 'all', sort: 'downloads' }; + +function showToast(message) { + const toast = $('[data-toast]'); toast.textContent = message; toast.classList.add('is-visible'); + clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('is-visible'), 2200); +} + +function bindMenu() { + const toggle = $('[data-menu-toggle]'); const menu = $('[data-mobile-menu]'); + toggle.addEventListener('click', () => { const open = menu.classList.toggle('is-open'); toggle.setAttribute('aria-expanded', String(open)); }); + $$('a', menu).forEach((link) => link.addEventListener('click', () => { menu.classList.remove('is-open'); toggle.setAttribute('aria-expanded', 'false'); })); +} + +function bindScrollEffects() { + const header = $('[data-sticky-header]'); const marker = document.createElement('div'); marker.className = 'scroll-marker'; document.body.prepend(marker); + new IntersectionObserver(([entry]) => header.classList.toggle('is-scrolled', !entry.isIntersecting), { threshold: 0 }).observe(marker); +} + +function sortedPackages() { + const query = state.query.trim().toLowerCase(); + return state.packages.filter((item) => { + const haystack = `${item.name} ${item.description} ${item.author}`.toLowerCase(); + return (state.type === 'all' || item.type === state.type) && (!query || haystack.includes(query)); + }).sort((a, b) => { + if (state.sort === 'name') return a.name.localeCompare(b.name); + if (state.sort === 'recent') return a.age - b.age; + return b.downloads - a.downloads; + }); +} + +function packageCard(item) { + return `
${item.type}${item.ageLabel}

${item.name}

${item.description}

${item.author}${item.downloadsLabel}/mo
${item.install}
`; +} + +function renderRecent() { + $('[data-recent-packages]').innerHTML = state.packages.slice(0, 4).map((item) => `${item.name}${item.description}${item.ageLabel}`).join(''); +} + +function renderList() { + const items = sortedPackages(); + $('[data-package-count]').textContent = state.packages.length; + $('[data-results-count]').textContent = `${items.length} 个结果`; + $('[data-package-list]').innerHTML = items.map(packageCard).join(''); + $('[data-empty-state]').hidden = items.length > 0; + $$('[data-copy-package]').forEach((button) => button.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(button.dataset.copyPackage); showToast('安装命令已复制'); } catch { showToast('复制失败,请手动选择文本'); } + })); +} + +async function init() { + const response = await fetch('./data/packages.json'); state.packages = await response.json(); + renderRecent(); renderList(); bindMenu(); bindScrollEffects(); + $('[data-package-search]').addEventListener('input', (event) => { state.query = event.target.value; renderList(); }); + $('[data-package-type]').addEventListener('change', (event) => { state.type = event.target.value; renderList(); }); + $('[data-package-sort]').addEventListener('change', (event) => { state.sort = event.target.value; renderList(); }); + $('[data-reset]').addEventListener('click', () => { state.query = ''; state.type = 'all'; state.sort = 'downloads'; $('[data-package-search]').value = ''; $('[data-package-type]').value = 'all'; $('[data-package-sort]').value = 'downloads'; renderList(); }); + $('[data-copy-command]').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('[data-copy-command]').previousElementSibling.textContent); showToast('安装命令已复制'); } catch { showToast('复制失败,请手动选择文本'); } }); +} + +init().catch((error) => { console.error(error); showToast('数据加载失败,请通过本地开发服务器打开网站'); }); diff --git a/website/styles.css b/website/styles.css new file mode 100644 index 0000000..7697efc --- /dev/null +++ b/website/styles.css @@ -0,0 +1,53 @@ +:root { + --ink: #101113; --paper: #f4f1e9; --paper-deep: #e9e5da; --muted: #6f706d; --line: rgba(16, 17, 19, .14); + --orange: #ec6b38; --orange-deep: #a84422; --white: #fffdf8; + --mono: 'DM Mono', 'SFMono-Regular', Consolas, monospace; --sans: 'Manrope', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; background: var(--paper); color: var(--ink); font-family: var(--sans); -webkit-font-smoothing: antialiased; } +a { color: inherit; text-decoration: none; } button { font: inherit; } .site-shell { overflow: hidden; } +.scroll-marker { position: absolute; top: 0; left: 0; width: 1px; height: 1px; } +.topbar { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 22px clamp(22px, 5vw, 76px); background: rgba(244,241,233,.94); transition: box-shadow .3s, padding .3s; } +.topbar.is-scrolled { box-shadow: 0 8px 25px rgba(16,17,19,.08); padding-top: 15px; padding-bottom: 15px; backdrop-filter: blur(12px); } +.brand, .footer-brand { display: inline-flex; align-items: center; gap: 10px; font-size: 23px; font-weight: 800; letter-spacing: -.06em; } +.brand-mark { display: inline-flex; align-items: flex-end; gap: 3px; height: 22px; }.brand-mark span { width: 5px; border-radius: 1px; background: var(--ink); display: block; }.brand-mark span:nth-child(1) { height: 13px; opacity: .55; }.brand-mark span:nth-child(2) { height: 19px; background: var(--orange); }.brand-mark span:nth-child(3) { height: 9px; opacity: .45; } +.desktop-nav { display: flex; gap: clamp(18px, 3vw, 40px); margin-left: 10vw; font-size: 12px; font-weight: 700; }.desktop-nav a, .text-link { transition: color .2s; }.desktop-nav a:hover, .text-link:hover { color: var(--orange-deep); } +.topbar-actions { display: flex; align-items: center; gap: 25px; }.text-link { font-size: 12px; font-weight: 700; }.button { display: inline-flex; align-items: center; justify-content: center; gap: 17px; min-height: 50px; padding: 0 22px; background: var(--ink); color: var(--white); border: 1px solid var(--ink); font-size: 12px; font-weight: 700; transition: transform .2s, background .2s, color .2s; }.button:hover { transform: translateY(-2px); background: var(--orange); border-color: var(--orange); }.button-small { min-height: 38px; padding: 0 16px; gap: 12px; font-size: 11px; }.button-ghost { color: var(--ink); background: transparent; }.button-ghost:hover { color: var(--white); } +.menu-toggle { display: none; border: 0; background: transparent; padding: 7px 0 7px 12px; cursor: pointer; }.menu-toggle span { display: block; width: 22px; height: 1px; background: var(--ink); margin: 5px 0; }.mobile-menu { display: none; } +.page-section { width: min(1320px, calc(100% - 44px)); margin: 0 auto; } +.hero { min-height: 680px; display: grid; grid-template-columns: minmax(0, .95fr) minmax(500px, 1.05fr); align-items: center; gap: clamp(30px, 5vw, 78px); padding-top: clamp(60px, 8vw, 105px); padding-bottom: clamp(70px, 10vw, 135px); } +.eyebrow { display: flex; align-items: center; gap: 10px; color: var(--orange); font-family: var(--mono); font-size: 10px; font-weight: 500; letter-spacing: .13em; }.eyebrow-dark { color: var(--muted); }.status-dot { width: 6px; height: 6px; background: var(--orange); border-radius: 50%; box-shadow: 0 0 0 5px rgba(236,107,56,.14); } +.hero h1 { max-width: 700px; margin: 28px 0 24px; font-size: clamp(52px, 6vw, 84px); line-height: .98; letter-spacing: -.08em; font-weight: 800; } .hero-title-line { display: inline-block; white-space: nowrap; } h1 em, h2 span { color: var(--orange); font-style: normal; }.hero-lede { max-width: 500px; margin: 0; color: var(--muted); font-size: 16px; line-height: 1.85; }.hero-actions { display: flex; gap: 11px; margin-top: 34px; }.hero-meta { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 34px; color: var(--muted); font-size: 10px; font-family: var(--mono); }.meta-dot { display: inline-block; width: 6px; height: 6px; margin-right: 7px; border-radius: 50%; }.green { background: #6eb195; }.orange { background: var(--orange); }.blue { background: #668eaf; } +.hero-visual { position: relative; padding: 30px 0 30px 30px; }.visual-glow { position: absolute; width: 75%; height: 75%; top: 12%; left: 14%; background: var(--orange); filter: blur(90px); opacity: .12; }.terminal-window { position: relative; overflow: hidden; background: #171819; color: #d9ddd3; box-shadow: 20px 28px 50px rgba(16,17,19,.18); }.terminal-bar { height: 48px; display: flex; align-items: center; padding: 0 17px; border-bottom: 1px solid rgba(255,255,255,.1); font-family: var(--mono); font-size: 10px; }.window-dots { display: flex; gap: 6px; margin-right: auto; }.window-dots i { width: 7px; height: 7px; display: block; border-radius: 50%; background: #555; }.window-dots i:first-child { background: #dd6c46; }.window-dots i:nth-child(2) { background: #cfa85c; }.window-dots i:nth-child(3) { background: #6ea690; }.terminal-title { position: absolute; left: 50%; transform: translateX(-50%); color: #858983; }.terminal-live { margin-left: auto; color: #7eb59d; font-size: 9px; }.terminal-live span { display: inline-block; width: 5px; height: 5px; margin-right: 5px; border-radius: 50%; background: #7eb59d; box-shadow: 0 0 0 3px rgba(126,181,157,.16); } +.terminal-body { min-height: 355px; padding: 28px 25px; font-family: var(--mono); font-size: 12px; line-height: 2.1; }.terminal-line { white-space: pre-wrap; }.terminal-muted, .syntax-dim { color: #6f756f; }.terminal-spacer { height: 14px; }.prompt-mark { color: var(--orange); margin-right: 10px; }.line-index { display: inline-block; width: 26px; color: #464b48; user-select: none; }.syntax-key { color: #b8d3c3; }.syntax-punct { color: #777e78; }.syntax-fn { color: #eca06f; }.syntax-text { color: #dedfce; }.syntax-string { color: #d9c886; }.syntax-comment { color: #7d9689; }.event-dot { display: inline-block; width: 5px; height: 5px; margin: 0 8px 2px 1px; border-radius: 50%; }.orange-fill { background: var(--orange); }.blue-fill { background: #86aece; }.terminal-success { color: #9ec7ae; }.terminal-cursor { display: inline-block; width: 7px; height: 14px; margin-left: 27px; background: var(--orange); vertical-align: -2px; animation: blink 1.1s steps(2) infinite; }.terminal-footer { display: flex; justify-content: space-between; padding: 11px 18px; color: #686e69; border-top: 1px solid rgba(255,255,255,.08); font: 9px var(--mono); text-transform: uppercase; letter-spacing: .08em; } @keyframes blink { 50% { opacity: 0; } } +.floating-stat { position: absolute; z-index: 2; display: flex; flex-direction: column; background: var(--white); box-shadow: 8px 12px 22px rgba(16,17,19,.1); padding: 13px 17px; min-width: 112px; }.stat-top { top: 3%; right: -8px; }.stat-bottom { bottom: 3%; left: -4px; }.stat-label { color: var(--muted); font: 8px var(--mono); letter-spacing: .1em; }.floating-stat strong { margin-top: 4px; font-size: 19px; letter-spacing: -.05em; }.floating-stat strong span { color: var(--orange); font: 9px var(--mono); letter-spacing: 0; } +.signal-strip { display: grid; grid-template-columns: repeat(4, 1fr); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); width: min(1320px, calc(100% - 44px)); margin: 0 auto; }.signal-item { padding: 25px 20px; border-right: 1px solid var(--line); }.signal-item:first-child { padding-left: 0; }.signal-item:last-child { border-right: 0; }.signal-item strong { display: block; font-size: 30px; line-height: 1; letter-spacing: -.08em; }.signal-item span { display: block; margin-top: 8px; color: var(--muted); font: 10px var(--mono); } +.intro { padding-top: clamp(100px, 13vw, 175px); padding-bottom: clamp(100px, 12vw, 160px); }.section-heading h2 { margin: 22px 0 0; font-size: clamp(37px, 5.2vw, 69px); line-height: 1.02; letter-spacing: -.08em; }.split-heading { display: grid; grid-template-columns: 1fr minmax(290px, .7fr); gap: 70px; align-items: end; }.split-heading > p { max-width: 450px; margin: 0 0 4px; color: var(--muted); font-size: 14px; line-height: 1.9; }.feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-top: 70px; }.feature-card { min-height: 265px; position: relative; display: flex; flex-direction: column; padding: 22px; background: var(--white); border: 1px solid rgba(16,17,19,.06); }.feature-top { display: flex; align-items: center; justify-content: space-between; }.feature-number, .feature-tag { font: 10px var(--mono); }.feature-number { color: var(--orange); }.feature-tag { color: var(--muted); }.feature-card h3 { margin: auto 0 12px; font-size: 20px; letter-spacing: -.05em; }.feature-card p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.7; }.feature-line { width: 32px; height: 2px; margin-top: 26px; background: var(--orange); } +.architecture-section { padding-bottom: clamp(100px, 13vw, 175px); }.centered-heading { text-align: center; }.centered-heading .eyebrow { justify-content: center; }.centered-heading > p { max-width: 500px; margin: 20px auto 0; color: var(--muted); font-size: 14px; line-height: 1.8; }.architecture-map { position: relative; max-width: 760px; margin: 60px auto 0; }.architecture-layer { position: relative; display: grid; grid-template-columns: 70px 165px 1fr; gap: 20px; align-items: center; min-height: 83px; padding: 15px 22px; background: var(--white); border-left: 4px solid var(--orange); }.architecture-layer + .architecture-layer { margin-top: 11px; }.layer-index { color: var(--muted); font: 10px var(--mono); }.layer-name { font: 15px var(--mono); font-weight: 500; }.layer-label { font-size: 13px; font-weight: 700; }.architecture-layer p { grid-column: 3; margin: 3px 0 0; color: var(--muted); font-size: 11px; }.layer-arrow { position: absolute; bottom: -18px; left: 50%; z-index: 2; display: grid; place-items: center; width: 24px; height: 24px; background: var(--paper); color: var(--orange); font: 15px var(--mono); transform: translateX(-50%); }.tone-mint { border-left-color: #75ad98; }.tone-blue { border-left-color: #7399ba; }.tone-violet { border-left-color: #9185bb; }.tone-cream { border-left-color: #b18e4a; } +.examples-section { padding-bottom: clamp(105px, 13vw, 175px); }.examples-heading { align-items: start; }.code-lab { display: grid; grid-template-columns: 220px 1fr; gap: 0; margin-top: 62px; background: #1c1e1e; box-shadow: 15px 20px 40px rgba(16,17,19,.12); }.code-tabs { display: flex; flex-direction: column; padding: 15px 0; border-right: 1px solid rgba(255,255,255,.1); }.code-tab { display: flex; align-items: center; gap: 10px; width: 100%; padding: 17px 19px; border: 0; border-left: 2px solid transparent; color: #777d78; background: transparent; text-align: left; cursor: pointer; font: 10px var(--mono); transition: background .2s, color .2s; }.code-tab:hover { color: #d3d7ca; }.code-tab.is-active { border-left-color: var(--orange); color: #f0f0e9; background: rgba(255,255,255,.06); }.tab-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }.code-panel { min-width: 0; }.code-panel-top { display: flex; justify-content: space-between; align-items: center; height: 50px; padding: 0 20px; border-bottom: 1px solid rgba(255,255,255,.1); }.code-file { color: #888f88; font: 10px var(--mono); }.copy-button { padding: 4px 0; border: 0; color: #aeb5ab; background: transparent; cursor: pointer; font: 10px var(--mono); }.copy-button:hover { color: var(--orange); }.code-panel pre { min-height: 315px; overflow: auto; margin: 0; padding: 26px 30px; color: #d2d7ca; font: 12px/2 var(--mono); tab-size: 4; }.code-panel-foot { display: flex; justify-content: space-between; gap: 15px; padding: 13px 20px; border-top: 1px solid rgba(255,255,255,.1); color: #6e766e; font: 9px var(--mono); }.code-panel-foot a { color: #d8a37b; } +.extension-section { padding-bottom: clamp(100px, 13vw, 165px); }.extension-card { display: grid; grid-template-columns: .86fr 1.14fr; overflow: hidden; min-height: 390px; background: var(--ink); color: var(--white); }.extension-copy { padding: clamp(35px, 6vw, 75px); }.extension-copy h2 { margin: 22px 0 18px; font-size: clamp(38px, 5vw, 65px); line-height: .99; letter-spacing: -.08em; }.extension-copy p { max-width: 410px; margin: 0; color: #9b9d98; font-size: 13px; line-height: 1.9; }.light-link { display: inline-block; margin-top: 28px; color: #e7ab85; }.extension-terminal { position: relative; display: flex; flex-direction: column; justify-content: center; gap: 21px; padding: 45px clamp(25px, 7vw, 100px); background: #202322; }.extension-row { display: grid; grid-template-columns: minmax(150px, 1fr) 35px minmax(110px, .65fr); gap: 10px; align-items: center; font: 12px var(--mono); }.extension-key { color: #a8cdbb; }.extension-arrow { color: var(--orange); }.extension-value { color: #e0c986; }.extension-corner { position: absolute; right: 22px; bottom: 19px; color: #6d756e; font: 9px var(--mono); letter-spacing: .12em; }.extension-corner span { color: #83b49e; margin-left: 8px; } +.release-section { padding-bottom: clamp(100px, 13vw, 170px); }.release-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: 60px; }.release-card { position: relative; min-height: 205px; padding: 23px; border-top: 2px solid var(--ink); background: var(--white); }.release-current { border-top-color: var(--orange); }.release-building { border-top-color: #75ad98; }.release-exploring { border-top-color: #8c9bb8; }.release-top { display: flex; justify-content: space-between; font: 10px var(--mono); }.release-version { color: var(--orange); }.release-status { color: var(--muted); text-transform: uppercase; }.release-card h3 { margin: 30px 0 10px; font-size: 19px; letter-spacing: -.05em; }.release-card p { max-width: 290px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.7; }.release-mark { position: absolute; right: 22px; bottom: 19px; color: var(--orange); font-size: 20px; } +.get-started-section { width: 100%; padding: clamp(75px, 9vw, 115px) 22px; background: var(--paper-deep); }.get-started-inner { width: min(770px, 100%); margin: 0 auto; text-align: center; }.get-started-inner h2 { margin: 20px 0 17px; font-size: clamp(40px, 5.7vw, 73px); line-height: .99; letter-spacing: -.08em; }.get-started-inner > p { max-width: 520px; margin: 0 auto; color: var(--muted); font-size: 14px; line-height: 1.8; }.command-box { display: flex; align-items: center; margin-top: 34px; padding: 0 13px 0 18px; min-height: 53px; background: var(--ink); color: #d7dcd0; text-align: left; }.command-prompt { margin-right: 10px; color: var(--orange); font: 14px var(--mono); }.command-box code { overflow: auto; flex: 1; white-space: nowrap; font: 10px var(--mono); }.command-box button { padding: 9px; border: 0; color: #aaaFA8; background: transparent; cursor: pointer; }.command-box button:hover { color: var(--orange); }.get-started-links { display: flex; justify-content: center; gap: 10px; margin-top: 25px; } +.command-stack { margin-top: 30px; text-align: left; }.command-label { margin: 18px 0 8px; color: var(--muted); font: 9px var(--mono); letter-spacing: .1em; text-transform: uppercase; }.command-stack .command-box { margin-top: 0; } +.footer { display: flex; align-items: center; justify-content: space-between; padding: 27px clamp(22px, 5vw, 76px); background: var(--ink); color: var(--white); }.footer .brand-mark span:nth-child(1), .footer .brand-mark span:nth-child(3) { background: #7d827d; }.footer-tagline { margin-left: 8px; color: #777b75; font: 10px var(--mono); letter-spacing: .05em; }.footer-links { display: flex; align-items: center; gap: 28px; color: #969b92; font: 10px var(--mono); }.footer-links a:hover { color: var(--orange); }.toast { position: fixed; z-index: 30; right: 22px; bottom: 22px; padding: 12px 16px; color: var(--white); background: var(--ink); box-shadow: 0 8px 25px rgba(16,17,19,.2); font: 10px var(--mono); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .2s, transform .2s; }.toast.is-visible { opacity: 1; transform: translateY(0); } +@media (max-width: 980px) { .desktop-nav { margin-left: 0; }.hero { grid-template-columns: 1fr; padding-top: 75px; }.hero-copy { max-width: 720px; }.hero-visual { width: min(720px, 100%); margin: 0 auto; padding-left: 20px; }.feature-grid { grid-template-columns: repeat(2, 1fr); }.extension-card { grid-template-columns: 1fr; }.extension-terminal { min-height: 280px; }.split-heading { gap: 35px; }.topbar-actions { gap: 15px; } } +@media (max-width: 680px) { + .desktop-nav, .desktop-only { display: none; }.menu-toggle { display: block; }.mobile-menu { display: flex; position: fixed; z-index: 19; top: 69px; left: 0; right: 0; flex-direction: column; padding: 13px 22px 24px; background: rgba(244,241,233,.98); border-bottom: 1px solid var(--line); transform: translateY(-120%); transition: transform .25s; }.mobile-menu.is-open { transform: translateY(0); }.mobile-menu a { padding: 15px 0; border-bottom: 1px solid var(--line); font-size: 12px; font-weight: 700; }.topbar { padding: 18px 22px; }.page-section, .signal-strip { width: min(100% - 36px, 1320px); }.hero { min-height: 0; padding-top: 66px; padding-bottom: 80px; }.hero h1 { font-size: clamp(47px, 14vw, 73px); }.hero-lede { font-size: 14px; }.hero-actions, .get-started-links { flex-wrap: wrap; }.hero-meta { gap: 11px; font-size: 9px; }.hero-visual { padding: 20px 0 28px; }.terminal-body { min-height: 320px; padding: 20px 15px; font-size: 10px; }.terminal-title { display: none; }.floating-stat { padding: 10px 12px; min-width: 91px; }.stat-top { right: -5px; }.stat-bottom { left: -5px; }.signal-strip { grid-template-columns: repeat(2, 1fr); }.signal-item, .signal-item:first-child { padding: 18px 12px; }.signal-item:nth-child(2) { border-right: 0; }.signal-item:nth-child(-n+2) { border-bottom: 1px solid var(--line); }.signal-item strong { font-size: 25px; }.signal-item span { font-size: 9px; }.split-heading { grid-template-columns: 1fr; gap: 25px; }.section-heading h2 { font-size: clamp(40px, 12vw, 58px); }.feature-grid { grid-template-columns: 1fr; margin-top: 43px; }.feature-card { min-height: 205px; }.architecture-map { margin-top: 42px; }.architecture-layer { grid-template-columns: 42px 1fr; gap: 8px 15px; min-height: 92px; padding: 15px; }.layer-label { grid-column: 2; }.architecture-layer p { grid-column: 2; margin-top: 0; }.layer-arrow { bottom: -18px; }.code-lab { grid-template-columns: 1fr; margin-top: 42px; }.code-tabs { flex-direction: row; overflow: auto; border-right: 0; border-bottom: 1px solid rgba(255,255,255,.1); }.code-tab { min-width: max-content; border-left: 0; border-bottom: 2px solid transparent; padding: 14px 16px; }.code-tab.is-active { border-left: 0; border-bottom-color: var(--orange); }.code-panel pre { min-height: 280px; padding: 21px 16px; font-size: 10px; }.code-panel-foot { flex-direction: column; gap: 8px; }.extension-copy { padding: 38px 24px; }.extension-terminal { padding: 35px 20px; gap: 17px; }.extension-row { grid-template-columns: 1fr 22px .8fr; font-size: 10px; }.release-grid { grid-template-columns: 1fr; margin-top: 42px; }.release-card { min-height: 180px; }.get-started-section { padding-left: 18px; padding-right: 18px; }.command-box { align-items: flex-start; padding-top: 15px; padding-bottom: 15px; }.command-box code { white-space: normal; line-height: 1.5; }.footer { align-items: flex-start; flex-direction: column; gap: 22px; padding: 28px 22px; }.footer-links { flex-wrap: wrap; gap: 15px 22px; }.footer-tagline { display: none; } +} + +/* Package catalog */ +.desktop-nav .is-current { color: var(--orange-deep); } +.packages-hero { padding-top: clamp(90px, 12vw, 155px); padding-bottom: clamp(75px, 10vw, 120px); }.packages-hero h1 { max-width: 800px; margin: 24px 0 18px; font-size: clamp(50px, 7vw, 90px); line-height: .98; letter-spacing: -.08em; }.packages-hero h1 em { color: var(--orange); font-style: normal; }.packages-hero > p { max-width: 565px; margin: 0; color: var(--muted); font-size: 15px; line-height: 1.85; }.packages-install { display: flex; align-items: center; width: min(520px, 100%); min-height: 53px; margin-top: 32px; padding: 0 13px 0 18px; color: #d7dcd0; background: var(--ink); }.packages-install code { flex: 1; font: 11px var(--mono); }.packages-install button { padding: 8px; border: 0; color: #aab0a7; background: transparent; cursor: pointer; }.packages-install button:hover { color: var(--orange); } +.package-catalog { padding-bottom: clamp(90px, 12vw, 160px); }.catalog-heading { display: grid; grid-template-columns: 1fr minmax(270px, .6fr); gap: 60px; align-items: end; }.catalog-heading h2 { margin: 20px 0 0; font-size: clamp(36px, 4.4vw, 60px); line-height: 1; letter-spacing: -.08em; }.catalog-heading > p { max-width: 420px; margin: 0 0 4px; color: var(--muted); font-size: 13px; line-height: 1.8; }.recent-packages { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 48px; padding: 15px; border: 1px solid var(--line); }.recent-package { min-height: 145px; display: flex; flex-direction: column; padding: 18px; background: rgba(255,253,248,.62); border: 1px solid rgba(16,17,19,.08); transition: transform .2s, border-color .2s; }.recent-package:hover { transform: translateY(-3px); border-color: var(--orange); }.recent-package strong { font: 12px var(--mono); }.recent-package span { margin-top: auto; color: var(--muted); font-size: 11px; line-height: 1.5; }.recent-package small { margin-top: 14px; color: var(--orange-deep); font: 9px var(--mono); letter-spacing: .08em; } +.catalog-toolbar { display: grid; grid-template-columns: 1fr 155px 155px; gap: 10px; margin-top: 64px; }.search-field, .select-field { display: flex; align-items: center; min-height: 48px; border: 1px solid var(--line); background: var(--white); }.search-field > span { padding: 0 13px 0 15px; color: var(--orange); font-size: 21px; line-height: 1; }.search-field input, .select-field select { width: 100%; min-width: 0; height: 46px; border: 0; outline: 0; color: var(--ink); background: transparent; font: 11px var(--mono); }.search-field input::placeholder { color: #9a9b96; }.select-field { padding-left: 13px; gap: 8px; }.select-field span { color: var(--muted); font: 9px var(--mono); }.select-field select { cursor: pointer; }.catalog-meta { display: flex; justify-content: space-between; align-items: center; margin: 18px 0 10px; color: var(--muted); font: 10px var(--mono); }.reset-button { padding: 0; border: 0; color: var(--orange-deep); background: transparent; cursor: pointer; font: inherit; }.reset-button:hover { color: var(--ink); }.package-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }.package-card { min-height: 257px; display: flex; flex-direction: column; padding: 22px; background: var(--white); border: 1px solid rgba(16,17,19,.08); }.package-card-top, .package-card-bottom { display: flex; justify-content: space-between; align-items: center; }.package-type, .package-age { font: 9px var(--mono); letter-spacing: .08em; text-transform: uppercase; }.package-type { color: var(--orange-deep); }.type-provider { color: #557f9e; }.type-skill { color: #588b79; }.type-extension { color: #7a6aa2; }.type-prompt { color: #9b7d3e; }.package-age { color: var(--muted); }.package-card h3 { margin: 26px 0 9px; font: 16px var(--mono); }.package-card h3 a:hover { color: var(--orange-deep); }.package-card h3 a span { color: var(--orange); font-size: 13px; }.package-card > p { max-width: 540px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.65; }.package-card-bottom { margin-top: auto; padding-top: 18px; color: var(--muted); font: 10px var(--mono); }.package-downloads { color: var(--orange-deep); }.package-command { display: flex; align-items: center; gap: 8px; min-height: 34px; margin-top: 14px; padding: 0 9px 0 11px; color: #777e78; background: var(--paper); }.package-command code { overflow: hidden; flex: 1; text-overflow: ellipsis; white-space: nowrap; font: 9px var(--mono); }.package-command button { padding: 5px 0 5px 5px; border: 0; color: var(--orange-deep); background: transparent; cursor: pointer; }.empty-state { padding: 45px; border: 1px dashed var(--line); color: var(--muted); text-align: center; font: 11px var(--mono); } +@media (max-width: 980px) { .recent-packages { grid-template-columns: repeat(2, 1fr); }.catalog-heading { gap: 35px; } } +@media (max-width: 680px) { .packages-hero { padding-top: 70px; }.packages-hero h1 { font-size: clamp(45px, 13vw, 70px); }.catalog-heading { grid-template-columns: 1fr; gap: 25px; }.recent-packages { grid-template-columns: 1fr; margin-top: 38px; }.catalog-toolbar { grid-template-columns: 1fr; margin-top: 45px; }.package-list { grid-template-columns: 1fr; }.package-card { min-height: 240px; } } + +/* Documentation */ +.docs-hero { padding-top: clamp(76px, 10vw, 125px); padding-bottom: clamp(55px, 7vw, 90px); }.docs-hero h1 { margin: 22px 0 17px; font-size: clamp(52px, 8vw, 104px); line-height: .94; letter-spacing: -.085em; }.docs-hero > p { max-width: 610px; margin: 0; color: var(--muted); font-size: 15px; line-height: 1.85; } +.docs-workspace { padding-bottom: clamp(95px, 12vw, 160px); }.docs-toolbar { display: flex; align-items: stretch; justify-content: space-between; gap: 20px; padding: 15px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }.docs-modes { display: flex; gap: 5px; }.docs-mode { display: inline-flex; align-items: center; gap: 11px; min-height: 43px; padding: 0 16px; border: 1px solid transparent; color: var(--muted); background: transparent; cursor: pointer; font: 11px var(--mono); transition: color .2s, background .2s, border-color .2s; }.docs-mode span { color: var(--orange); font-size: 9px; }.docs-mode:hover { color: var(--ink); }.docs-mode.is-active { color: var(--ink); background: var(--white); border-color: var(--line); }.docs-search { display: flex; align-items: center; width: min(315px, 100%); min-height: 43px; border: 1px solid var(--line); background: var(--white); }.docs-search > span { padding: 0 12px 0 14px; color: var(--orange); font-size: 21px; }.docs-search input { width: 100%; height: 41px; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--ink); font: 10px var(--mono); }.docs-search input::placeholder { color: #9a9b96; } +.docs-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: clamp(38px, 7vw, 100px); margin-top: 45px; }.docs-sidebar { position: sticky; top: 100px; align-self: start; }.docs-sidebar-label { margin-bottom: 17px; color: var(--muted); font: 9px var(--mono); letter-spacing: .12em; }.docs-sidebar nav { display: flex; flex-direction: column; border-left: 1px solid var(--line); }.docs-sidebar nav a { display: flex; align-items: baseline; gap: 10px; padding: 9px 13px; color: var(--muted); font: 11px/1.35 var(--mono); border-left: 2px solid transparent; transform: translateX(-1px); transition: color .2s, border-color .2s, background .2s; }.docs-sidebar nav a span { min-width: 17px; color: var(--orange); font-size: 9px; }.docs-sidebar nav a:hover { color: var(--ink); background: rgba(255,253,248,.55); }.docs-repo-link { display: inline-block; margin-top: 28px; color: var(--orange-deep); font: 10px var(--mono); }.docs-repo-link:hover { color: var(--ink); } +.docs-mode-intro { display: grid; grid-template-columns: 1fr minmax(220px, .72fr); gap: 35px; align-items: end; margin-bottom: 60px; padding-bottom: 23px; border-bottom: 2px solid var(--ink); }.docs-mode-intro p { max-width: 355px; margin: 0; color: var(--muted); font-size: 12px; line-height: 1.8; }.docs-section { display: grid; grid-template-columns: minmax(190px, .64fr) minmax(0, 1fr); gap: clamp(25px, 5vw, 76px); padding: 0 0 62px; scroll-margin-top: 105px; }.docs-section + .docs-section { padding-top: 63px; border-top: 1px solid var(--line); }.docs-section-heading h2 { margin: 18px 0 12px; font-size: clamp(27px, 3.1vw, 43px); line-height: 1; letter-spacing: -.07em; }.docs-summary { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.75; }.docs-section-body > p { margin: 0 0 15px; color: #464844; font-size: 13px; line-height: 1.85; }.docs-code { margin: 28px 0 19px; overflow: hidden; background: var(--ink); color: #d7dcd0; box-shadow: 10px 14px 25px rgba(16,17,19,.09); }.docs-code-top { display: flex; align-items: center; justify-content: space-between; min-height: 42px; padding: 0 14px 0 17px; border-bottom: 1px solid rgba(255,255,255,.1); color: #858d84; font: 9px var(--mono); }.docs-code-top button { border: 0; color: #aeb5ab; background: transparent; cursor: pointer; font: 9px var(--mono); }.docs-code-top button:hover { color: var(--orange); }.docs-code pre { overflow: auto; margin: 0; padding: 19px 20px 22px; font: 11px/1.8 var(--mono); white-space: pre; tab-size: 4; }.docs-links { display: flex; flex-wrap: wrap; gap: 9px 20px; margin-top: 22px; }.docs-links a { color: var(--orange-deep); font: 10px var(--mono); }.docs-links a:hover { color: var(--ink); }.docs-no-results { padding: 46px 20px; border: 1px dashed var(--line); color: var(--muted); text-align: center; font: 11px var(--mono); } +@media (max-width: 800px) { .docs-layout { grid-template-columns: 1fr; gap: 40px; }.docs-sidebar { position: static; }.docs-sidebar nav { display: grid; grid-template-columns: repeat(2, 1fr); border-left: 0; border-top: 1px solid var(--line); }.docs-sidebar nav a { border-left: 0; border-bottom: 1px solid var(--line); padding: 11px 8px 11px 0; }.docs-repo-link { margin-top: 17px; }.docs-mode-intro { grid-template-columns: 1fr; gap: 16px; margin-bottom: 43px; }.docs-section { grid-template-columns: 1fr; gap: 22px; padding-bottom: 45px; }.docs-section + .docs-section { padding-top: 45px; } } +@media (max-width: 680px) { .docs-hero { padding-top: 67px; }.docs-hero h1 { font-size: clamp(48px, 14vw, 76px); }.docs-toolbar { align-items: stretch; flex-direction: column; gap: 12px; }.docs-modes { display: grid; grid-template-columns: 1fr 1fr; }.docs-mode { justify-content: center; padding: 0 7px; }.docs-search { width: 100%; }.docs-sidebar nav { grid-template-columns: 1fr; }.docs-sidebar nav a { padding: 10px 0; }.docs-section-heading h2 { font-size: 35px; }.docs-section-body > p { font-size: 12px; }.docs-code pre { padding: 16px; font-size: 10px; } } From 122c8e6f1ea2fa48feab366d2745fa500516c8e5 Mon Sep 17 00:00:00 2001 From: bigfish Date: Wed, 9 Sep 2026 23:29:38 +0800 Subject: [PATCH 4/4] ci: add workspace test workflow --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c1c4912 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + test: + name: Cargo test + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install stable Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + + - name: Run workspace tests + run: cargo test --workspace