Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Test

# 快速测试:每次 push / PR 跑 Rust 单测 + 前端类型检查/构建
# 跑在 macOS(本项目的主平台,无需额外系统依赖)
# CI:前端(类型检查 + 构建)与 Rust(fmt + clippy + 单测)并行
# Rust job 跑 macOS(本项目主平台,含 macOS 专属代码);前端纯 JS,跑 ubuntu 更快省时
on:
push:
branches: [main]
Expand All @@ -12,8 +12,9 @@ concurrency:
cancel-in-progress: true

jobs:
test:
runs-on: macos-latest
frontend:
name: Frontend — type-check + build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Expand All @@ -28,19 +29,44 @@ jobs:
node-version: 22
cache: pnpm

- name: Setup Rust
- name: Install JS deps
run: pnpm install --frozen-lockfile

- name: Type-check (tsc --noEmit)
run: pnpm exec tsc --noEmit

- name: Build (tsc + vite)
run: pnpm build

rust:
name: Rust — fmt + clippy + test
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

- name: Setup Rust (+ rustfmt, clippy)
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri

- name: Install JS deps
run: pnpm install --frozen-lockfile
- name: Format check
run: cargo fmt --manifest-path src-tauri/Cargo.toml --all --check

- name: Rust unit tests
- name: Clippy (deny warnings)
run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --all-features -- -D warnings

- name: Unit tests
run: cargo test --manifest-path src-tauri/Cargo.toml

- name: Frontend type-check + build
run: pnpm build
# 聚合门禁:满足分支保护要求的必需 check「test」(两个并行 job 都绿才绿)。
test:
name: test
needs: [frontend, rust]
runs-on: ubuntu-latest
steps:
- run: echo "frontend + rust 均通过"
718 changes: 718 additions & 0 deletions design/agent-workspace.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"tauri": "tauri"
},
"dependencies": {
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/space-grotesk": "^5.2.10",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"dialog:default",
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$HOME/**" }]
"allow": [{ "path": "$HOME/**" }, { "path": "**" }]
}
]
}
79 changes: 59 additions & 20 deletions src-tauri/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,51 @@ pub struct Usage {
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum DomainEvent {
#[serde(rename_all = "camelCase")]
Started { task_id: String, argv: Vec<String> },
Started {
task_id: String,
argv: Vec<String>,
},
#[serde(rename_all = "camelCase")]
ThreadStarted { thread_id: String },
ThreadStarted {
thread_id: String,
},
TurnStarted,
Reasoning { text: String },
AssistantMessage { text: String },
CommandRun { command: String, status: Option<String> },
Reasoning {
text: String,
},
AssistantMessage {
text: String,
},
CommandRun {
command: String,
status: Option<String>,
},
/// 产物发现:codex 改了文件。
#[serde(rename_all = "camelCase")]
Artifact { path: String, change_kind: String },
Plan { steps: serde_json::Value },
Artifact {
path: String,
change_kind: String,
},
Plan {
steps: serde_json::Value,
},
/// 轻量进度(如 todo_list 更新),不刷屏。
Progress { text: String },
TurnCompleted { usage: Usage },
Progress {
text: String,
},
TurnCompleted {
usage: Usage,
},
/// 容错透传:未知事件或解析失败,原样给前端控制台。
#[serde(rename_all = "camelCase")]
Raw { codex_type: String, json: serde_json::Value },
EngineError { class: String, message: String },
Raw {
codex_type: String,
json: serde_json::Value,
},
EngineError {
class: String,
message: String,
},
#[serde(rename_all = "camelCase")]
Finished {
outcome: String, // success | failure | cancelled
Expand Down Expand Up @@ -185,7 +212,10 @@ pub fn run_task(
// task 私有目录,用于 -o last_message.txt
let task_dir = std::env::temp_dir().join("nature-app").join(&task_id);
std::fs::create_dir_all(&task_dir).map_err(|e| format!("create task dir: {e}"))?;
let last_message_path = task_dir.join("last_message.txt").to_string_lossy().to_string();
let last_message_path = task_dir
.join("last_message.txt")
.to_string_lossy()
.to_string();

let bin = resolve_codex_bin();
let argv = build_argv(&spec, &last_message_path);
Expand Down Expand Up @@ -301,7 +331,10 @@ pub fn run_task(
if !still_running {
break; // 进程已退出,交给 stdout 线程收尾
}
let idle = wd_activity.lock().map(|t| t.elapsed().as_secs()).unwrap_or(0);
let idle = wd_activity
.lock()
.map(|t| t.elapsed().as_secs())
.unwrap_or(0);
if idle > IDLE_LIMIT_SECS {
wd_handle.cancelled.store(true, Ordering::SeqCst);
wd_ch
Expand Down Expand Up @@ -493,9 +526,7 @@ fn map_line(
message: msg,
}]
}
"item.started" | "item.completed" | "item.updated" => {
map_item(&v, t == "item.completed")
}
"item.started" | "item.completed" | "item.updated" => map_item(&v, t == "item.completed"),
other => vec![DomainEvent::Raw {
codex_type: other.to_string(),
json: v,
Expand Down Expand Up @@ -724,7 +755,9 @@ mod tests {
&mut u,
);
assert_eq!(tid.as_deref(), Some("abc-123"));
assert!(matches!(&evs[0], DomainEvent::ThreadStarted { thread_id } if thread_id == "abc-123"));
assert!(
matches!(&evs[0], DomainEvent::ThreadStarted { thread_id } if thread_id == "abc-123")
);
}

#[test]
Expand All @@ -737,7 +770,9 @@ mod tests {
&mut u,
);
assert_eq!(u.input_tokens, 44467);
assert!(matches!(&evs[0], DomainEvent::TurnCompleted { usage } if usage.output_tokens == 74));
assert!(
matches!(&evs[0], DomainEvent::TurnCompleted { usage } if usage.output_tokens == 74)
);
}

#[test]
Expand Down Expand Up @@ -769,15 +804,19 @@ mod tests {
&mut tid,
&mut u,
);
assert!(matches!(&evs[0], DomainEvent::AssistantMessage { text } if text.contains("chart.png")));
assert!(
matches!(&evs[0], DomainEvent::AssistantMessage { text } if text.contains("chart.png"))
);
}

#[test]
fn unknown_type_becomes_raw() {
let mut tid = None;
let mut u = Usage::default();
let evs = map_line(r#"{"type":"some.future.event","foo":1}"#, &mut tid, &mut u);
assert!(matches!(&evs[0], DomainEvent::Raw { codex_type, .. } if codex_type == "some.future.event"));
assert!(
matches!(&evs[0], DomainEvent::Raw { codex_type, .. } if codex_type == "some.future.event")
);
}

#[test]
Expand Down
10 changes: 8 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,16 @@ fn check_doctor() -> DoctorReport {
&["soffice", "libreoffice"],
&["/Applications/LibreOffice.app/Contents/MacOS/soffice"],
);
let pandoc = find_bin(&["pandoc"], &["/opt/homebrew/bin/pandoc", "/usr/local/bin/pandoc"]);
let pandoc = find_bin(
&["pandoc"],
&["/opt/homebrew/bin/pandoc", "/usr/local/bin/pandoc"],
);
let pdflatex = find_bin(
&["pdflatex"],
&["/Library/TeX/texbin/pdflatex", "/usr/local/texlive/bin/pdflatex"],
&[
"/Library/TeX/texbin/pdflatex",
"/usr/local/texlive/bin/pdflatex",
],
);
let tools = vec![
ToolCheck {
Expand Down
3 changes: 1 addition & 2 deletions src-tauri/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ pub async fn preview_plot(
.map_err(|e| format!("写 plot_data.json 失败: {e}"))?;

// 5. 复制模板为 plot.py(模板从脚本同目录读 JSON)
fs::copy(&template_path, work.join("plot.py"))
.map_err(|e| format!("复制模板失败: {e}"))?;
fs::copy(&template_path, work.join("plot.py")).map_err(|e| format!("复制模板失败: {e}"))?;

// 6. 获取 python(优先已就绪的 uv venv,回退系统 python3)
let python: PathBuf = if pyenv::is_ready() {
Expand Down
21 changes: 17 additions & 4 deletions src-tauri/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! - manifest.yaml:`always_load`(路径列表)、`axes.<name>.{detect, values(有序 map), default?, multi?}`、
//! `references.on_demand[].{condition, path}`。
//! - 三档 formCapability:有 axes → axes;有 manifest 无 axes → manifestNoAxes;无 manifest(reviewer)→ proseOnly。
//!
//! 用 serde_yaml::Value 防御式读取,容忍各 skill 的 schema 差异。

use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -111,7 +112,10 @@ fn parse_axes(m: &serde_yaml::Value) -> Vec<Axis> {
})
.unwrap_or_default();
let multi = av.get("multi").and_then(|x| x.as_bool()).unwrap_or(false);
let default_value = av.get("default").and_then(|x| x.as_str()).map(|s| s.to_string());
let default_value = av
.get("default")
.and_then(|x| x.as_str())
.map(|s| s.to_string());
out.push(Axis {
name,
values,
Expand All @@ -132,7 +136,11 @@ fn parse_on_demand(m: &serde_yaml::Value) -> Vec<OnDemandRef> {
.filter_map(|e| {
Some(OnDemandRef {
condition: e.get("condition").and_then(|c| c.as_str())?.to_string(),
path: e.get("path").and_then(|p| p.as_str()).unwrap_or("").to_string(),
path: e
.get("path")
.and_then(|p| p.as_str())
.unwrap_or("")
.to_string(),
})
})
.collect()
Expand Down Expand Up @@ -288,7 +296,12 @@ mod tests {
fn loads_all_eleven_nature_skills() {
let skills = load_skills(&root());
// 11 个 nature-* skill
assert_eq!(skills.len(), 11, "应解析 11 个 nature-* skill,实际 {}", skills.len());
assert_eq!(
skills.len(),
11,
"应解析 11 个 nature-* skill,实际 {}",
skills.len()
);
assert!(skills.iter().all(|s| s.id.starts_with("nature-")));
}

Expand All @@ -302,7 +315,7 @@ mod tests {
assert!(backend.blocking_gate);
assert!(!backend.multi);
assert_eq!(backend.values, vec!["python", "r"]); // 保持声明顺序
// 描述应来自 SKILL.md frontmatter(含中文触发词)
// 描述应来自 SKILL.md frontmatter(含中文触发词)
assert!(fig.description.contains("科研绘图") || fig.description.contains("figure"));
}

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"devCsp": "default-src 'self'; img-src 'self' asset: http://asset.localhost data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' ws: http://localhost:1420 ipc: http://ipc.localhost",
"assetProtocol": {
"enable": true,
"scope": ["$HOME/**"]
"scope": ["$HOME/**", "**"]
}
}
},
Expand Down
Loading
Loading