diff --git a/AGENTS.md b/AGENTS.md index f19d4a4..70d4d02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,12 +96,14 @@ let b = self.buf.pop_front(); | crate | 推荐 tokio features | |---|---| -| `focus-core` | `rt`, `sync`, `macros` | -| `focus-transport` | `rt`, `net`, `io-util`, `sync` | -| `focus-providers` | `rt`, `sync`, `macros` | +| `focus-core` | (无 tokio —— 核心层为纯同步逻辑) | +| `focus-transport` | `net`, `io-util`, `time`(TCP/TLS、超时) | +| `focus-providers` | `rt`, `time`(后台 current-thread 运行时) | | `focus-tools` | (无 tokio —— 工具为同步 `std` I/O) | | `focus-harness` | (无 tokio —— 会话/压缩为纯逻辑 + 同步 `std` I/O) | +> workspace 级 `tokio` 声明不带 features;由各成员按上表自行声明。 + ### 2.3 Rust 版本 - 使用**最新 stable** Rust。 @@ -189,8 +191,9 @@ focus-tui ← focus-core, focus-providers, focus-tools, focus-harness, f (`~/.focus/config.json`,TUI 内 `/config` 编辑;provider 构造时传入)、动态系统 提示注入(方案 E 的 Usage/上下文占用)、自动上下文压缩的执行(harness 产出方案 → 调 provider 摘要 → `Agent::replace_messages` 写回)、会话持久化(`~/.focus/sessions/`)。 -- **交互**:多行输入、流式显示、Esc 中止、折叠的思考/工具块(显示耗时与内容摘要, - 点击或 Tab 展开)、命令(`/new` `/sessions` `/config` `/compact`)。 +- **交互**:多行输入、Markdown 渲染、流式显示、Esc 中止、折叠的思考/工具块 + (显示耗时与内容摘要,点击或 Tab 两级展开——先预览后全量)、工具请求与结果合并 + 为一块、命令(`/new` `/sessions` `/config` `/compact`)。 - **禁止**:把业务逻辑放这里。这里只做**组装与呈现**。 ## 4. 代码规范 diff --git a/Cargo.lock b/Cargo.lock index 386b4db..5c113a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,7 +429,6 @@ name = "focus-core" version = "0.1.0" dependencies = [ "focus-json", - "tokio", ] [[package]] @@ -1702,23 +1701,10 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2", - "tokio-macros", "windows-sys 0.61.2", ] -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "tokio-rustls" version = "0.26.4" diff --git a/Cargo.toml b/Cargo.toml index 908cb34..b5f2e20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,9 @@ license = "MIT" [workspace.dependencies] # --- allowed runtime dependencies (see AGENTS.md §2.1) --------------------- -tokio = { version = "1", features = ["rt", "sync", "macros", "net", "io-util", "fs", "process", "rt-multi-thread", "signal", "time"] } +# tokio features 由各成员 crate 按需声明(见 AGENTS.md §2.2)。 +# Each member declares the tokio features it actually needs (see AGENTS.md §2.2). +tokio = { version = "1" } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] } rustls-native-certs = "0.7" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..87aaa5a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 focus contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d81c231..d8574f3 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ focus-tui ← focus-core, focus-providers, focus-tools, focus-harness, # 构建整个 workspace cargo build --workspace -# 运行全部测试(177 个用例,全绿) +# 运行全部测试(181 个用例,全绿) cargo test --workspace # 代码质量检查 @@ -116,6 +116,14 @@ cargo run -p focus-tui | 配置 | `/config.json` | | 会话 | `/sessions/.jsonl`(JSONL 追加写,树结构) | +### 环境变量 + +| 变量 | 说明 | +|---|---| +| `FOCUS_DATA_DIR` | 覆盖数据根目录(默认 `~/.focus`) | +| `FOCUS_SHELL` | 覆盖 `shell` 工具使用的 shell 程序(Windows 默认 PowerShell、可回退 cmd;Linux 默认 bash) | +| `FOCUS_DEBUG_FILE` | 设置后,providers 把每个原始网络 chunk 追加到该文件(排查协议不兼容,如推理字段名差异) | + ## ⌨️ TUI 使用 ### 命令 @@ -137,7 +145,8 @@ cargo run -p focus-tui | `Esc` | 运行中中止任务;空闲时退出 | | `Ctrl+C` | 退出 | | `Ctrl+D` | 继续(追问一轮) | -| `Tab` | 展开 / 折叠最近的思考或工具块 | +| `Tab` | 展开 / 折叠最近的思考或工具块(两级:先预览,再全量) | +| 鼠标滚轮 | 逐行滚动(Linux / Windows 均支持;上滚暂停自动跟随) | | `Ctrl+U` / `PageUp` / `PageDown` | 滚动消息区(输入为空时 `↑` `↓` 也可滚动) | ## 🧪 测试 @@ -145,7 +154,7 @@ cargo run -p focus-tui - 所有测试位于各 crate 的 `crates//tests/`,禁止在 `src/` 内联测试 - 核心层(`focus-core`)测试**零网络依赖**,用 mock provider 驱动 - provider 测试用**录制 / 回放**的 mock transport,不打真实 API(真实 API 测试用 `#[ignore]` 标记) -- 当前共 **177 个测试用例**,`cargo test --workspace` 全绿 +- 当前共 **181 个测试用例**,`cargo test --workspace` 全绿 ## 📚 文档 diff --git a/crates/focus-core/Cargo.toml b/crates/focus-core/Cargo.toml index d64bb3d..a7b2ddf 100644 --- a/crates/focus-core/Cargo.toml +++ b/crates/focus-core/Cargo.toml @@ -7,5 +7,6 @@ license.workspace = true description = "Core agent layer: domain types, agent loop, tool and provider traits" [dependencies] +# 核心层是纯同步逻辑(同步拉取式流模型),不依赖任何运行时。 +# The core layer is purely synchronous (pull-based streaming); no runtime. focus-json.workspace = true -tokio = { workspace = true, features = ["rt", "sync", "macros"] } diff --git a/crates/focus-core/src/agent.rs b/crates/focus-core/src/agent.rs index 95d47e7..d9c3c11 100644 --- a/crates/focus-core/src/agent.rs +++ b/crates/focus-core/src/agent.rs @@ -434,10 +434,6 @@ impl std::fmt::Debug for Agent { } } -/// 一次运行返回的句柄(目前仅作信息用途)。 -/// A handle returned from a run (currently just informational). -pub struct AgentHandle; - /// 一批已执行工具调用的结果。 /// The results of one executed batch of tool calls. struct ExecutedBatch { diff --git a/crates/focus-core/src/lib.rs b/crates/focus-core/src/lib.rs index 59912c2..2f18578 100644 --- a/crates/focus-core/src/lib.rs +++ b/crates/focus-core/src/lib.rs @@ -33,7 +33,7 @@ pub mod provider; /// The `Tool` trait, effects bitflag, and tool registry. pub mod tool; -pub use agent::{Agent, AgentConfig, AgentHandle}; +pub use agent::{Agent, AgentConfig}; pub use error::{CoreError, CoreResult}; pub use event::{AgentEvent, EventSink}; pub use model::*; diff --git a/crates/focus-providers/Cargo.toml b/crates/focus-providers/Cargo.toml index 45fd6e6..26c5709 100644 --- a/crates/focus-providers/Cargo.toml +++ b/crates/focus-providers/Cargo.toml @@ -10,7 +10,7 @@ description = "LLM provider implementations (Anthropic, OpenAI) for the focus ag focus-core.workspace = true focus-transport.workspace = true focus-json.workspace = true -tokio = { workspace = true, features = ["rt", "sync", "macros"] } +tokio = { workspace = true, features = ["rt", "time"] } [dev-dependencies] focus-harness.workspace = true diff --git a/crates/focus-providers/tests/compaction_integration.rs b/crates/focus-providers/tests/compaction_integration.rs index 5922461..6394efb 100644 --- a/crates/focus-providers/tests/compaction_integration.rs +++ b/crates/focus-providers/tests/compaction_integration.rs @@ -165,10 +165,9 @@ fn full_conversation_compaction_and_continue() { // Re-inject a provider (the mock is single-use). let provider = AnthropicProvider::with_transport(ProviderConfig::new("sk-test"), Arc::new(mock3)); - agent.replace_messages(agent.messages().to_vec()); - // 通过内部字段替换 provider:直接重建 agent 以保留消息。 - // Rebuild the agent keeping the transcript, since the provider is not - // swappable on an existing agent. + // provider 不可在既有 agent 上替换,重建 agent 并保留 transcript。 + // The provider is not swappable on an existing agent; rebuild it keeping + // the transcript. let mut agent2 = Agent::new( AgentConfig { model: "claude-sonnet-4".into(), diff --git a/crates/focus-transport/Cargo.toml b/crates/focus-transport/Cargo.toml index 0be145a..2a07cfc 100644 --- a/crates/focus-transport/Cargo.toml +++ b/crates/focus-transport/Cargo.toml @@ -8,7 +8,7 @@ description = "Transport layer: HTTPS client (tokio + rustls), HTTP/1.1, SSE par [dependencies] focus-json.workspace = true -tokio = { workspace = true, features = ["rt", "net", "io-util", "sync"] } +tokio = { workspace = true, features = ["net", "io-util", "time"] } rustls.workspace = true tokio-rustls.workspace = true rustls-native-certs.workspace = true diff --git a/crates/focus-tui/src/app.rs b/crates/focus-tui/src/app.rs index 0d7caf5..a51c008 100644 --- a/crates/focus-tui/src/app.rs +++ b/crates/focus-tui/src/app.rs @@ -918,14 +918,6 @@ impl App { } } - /// 把输入框的滚动对齐到底部(跟随模式)。 - /// Align the scroll to the bottom when following. - pub fn clamp_scroll(&mut self) { - if self.follow { - self.scroll = self.max_scroll(); - } - } - /// 最大滚动偏移(由最近渲染的总行数决定;几何只含可展开块,不能用作依据)。 /// Max scroll offset (based on the last render's total line count; the /// geometry only covers expandable blocks and is unusable for this). diff --git a/crates/focus-tui/src/job.rs b/crates/focus-tui/src/job.rs index 3c1d1b6..191dd73 100644 --- a/crates/focus-tui/src/job.rs +++ b/crates/focus-tui/src/job.rs @@ -113,6 +113,10 @@ fn run_job(job: RunJob, tx: Sender) { if let Some(plan) = plan_compaction(&transcript, window, COMPACT_THRESHOLD, DEFAULT_KEEP_RATIO) { + let _ = tx.send(JobEvent::Note(format!( + "compacting… ({})", + focus_harness::compaction::describe_plan(&plan) + ))); match summarize(provider.as_ref(), &job.config, &plan) { Ok(summary) => { transcript = apply_summary(&summary, &plan); diff --git a/crates/focus-tui/src/ui.rs b/crates/focus-tui/src/ui.rs index ecd578b..864b512 100644 --- a/crates/focus-tui/src/ui.rs +++ b/crates/focus-tui/src/ui.rs @@ -518,7 +518,8 @@ focus — 终端编码 agent /help 本帮助 思考块与工具调用默认折叠,只显示摘要(耗时 / 内容); -点击或 Tab 可查看完整细节。"; +点击或 Tab 展开:第一次显示预览(剩余行数提示),再次展开显示全部; +工具块把请求参数与执行结果合并展示。"; fn draw_help_modal(frame: &mut Frame, area: Rect) { let rect = centered_rect(70, 70, area);