feat(tui): streamline client filtering and model-family presentation - #187
Conversation
WalkthroughPR 将 TUI 模型身份统一为 Changes模型身份与颜色
缓存 schema 44
客户端目录与选择器
概览布局
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant ClientPickerDialog
participant ClientCatalog
participant SelectedClients
Operator->>ClientPickerDialog: 输入过滤或按 *
ClientPickerDialog->>ClientCatalog: 获取匹配客户端
ClientPickerDialog->>ClientPickerDialog: 更新本地 draft
ClientPickerDialog->>SelectedClients: 提交非空选择
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/tokscale-cli/src/tui/config.rs (1)
105-121: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win为顶层配置加上未知字段拒绝
TokscaleConfig会静默忽略顶层未知字段,旧版[colors]段会直接失效。给TokscaleConfig加#[serde(deny_unknown_fields)],或补一条覆盖[colors]的回归测试。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/tokscale-cli/src/tui/config.rs` around lines 105 - 121, 为 TokscaleConfig 增加 #[serde(deny_unknown_fields)],使顶层未知配置字段(包括旧版 [colors] 段)加载时返回错误;同时保留现有 malformed_or_unreadable_config_is_explicit 测试行为。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/tokscale-cli/src/tui/colors.rs`:
- Around line 47-68: Update parse_catalog_color to return an optional result
instead of panicking when the color lacks the # prefix, has an invalid length,
or contains non-hexadecimal characters. In get_client_color, fall back to
UNKNOWN_CLIENT_COLOR when parsing the known ClientId color fails, while
preserving the existing unknown-client fallback.
---
Outside diff comments:
In `@crates/tokscale-cli/src/tui/config.rs`:
- Around line 105-121: 为 TokscaleConfig 增加
#[serde(deny_unknown_fields)],使顶层未知配置字段(包括旧版 [colors] 段)加载时返回错误;同时保留现有
malformed_or_unreadable_config_is_explicit 测试行为。
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3d1b4a7f-9534-47ef-853c-fde02c9f2d3b
📒 Files selected for processing (32)
crates/tokscale-cli/src/tui/app.rscrates/tokscale-cli/src/tui/cache.rscrates/tokscale-cli/src/tui/colors.rscrates/tokscale-cli/src/tui/config.rscrates/tokscale-cli/src/tui/data/mod.rscrates/tokscale-cli/src/tui/data/overview.rscrates/tokscale-cli/src/tui/mod.rscrates/tokscale-cli/src/tui/model_family.rscrates/tokscale-cli/src/tui/ui/achievements.rscrates/tokscale-cli/src/tui/ui/daily.rscrates/tokscale-cli/src/tui/ui/dialog/client_picker.rscrates/tokscale-cli/src/tui/ui/hourly.rscrates/tokscale-cli/src/tui/ui/mod.rscrates/tokscale-cli/src/tui/ui/models.rscrates/tokscale-cli/src/tui/ui/overview.rscrates/tokscale-cli/src/tui/ui/overview_snapshot.rscrates/tokscale-cli/src/tui/ui/period.rscrates/tokscale-cli/src/tui/ui/portraits.rscrates/tokscale-cli/src/tui/ui/stats.rscrates/tokscale-core/build.rscrates/tokscale-core/client-catalog.jsoncrates/tokscale-core/src/aggregate/tui.rscrates/tokscale-core/src/clients.rscrates/tokscale-core/src/provider_identity.rscrates/tokscale-core/src/usage_views.rsdocs/adr/0007-client-identity-catalog.mddocs/adr/0008-single-copy-memory-pipeline.mddocs/adr/0012-local-client-exclusions.mddocs/adr/0026-group-by-view-scope-contract.mddocs/adr/0028-tui-client-universe-and-view-selection.mddocs/adr/0029-keep-claude-discovery-catalog-scoped.mddocs/adr/0030-client-input-and-data-health-terminology.md
💤 Files with no reviewable changes (5)
- crates/tokscale-cli/src/tui/ui/hourly.rs
- crates/tokscale-core/src/clients.rs
- crates/tokscale-core/client-catalog.json
- crates/tokscale-core/src/usage_views.rs
- crates/tokscale-core/src/aggregate/tui.rs
| fn parse_catalog_color(hex: &str) -> Color { | ||
| let hex = hex | ||
| .strip_prefix('#') | ||
| .expect("client catalog colors are validated as #RRGGBB"); | ||
| assert_eq!( | ||
| hex.len(), | ||
| 6, | ||
| "client catalog colors are validated as #RRGGBB" | ||
| ); | ||
| let component = |range| { | ||
| u8::from_str_radix(&hex[range], 16).expect("client catalog colors are validated as #RRGGBB") | ||
| }; | ||
| Color::Rgb(lerp(r), lerp(g), lerp(b)) | ||
| } | ||
|
|
||
| const ANTHROPIC_SHADES: [(u8, u8, u8); 7] = [ | ||
| (218, 119, 86), // #DA7756 | ||
| (223, 136, 107), // #DF886B | ||
| (227, 153, 128), // #E39980 | ||
| (232, 170, 149), // #E8AA95 | ||
| (236, 184, 166), // #ECB8A6 | ||
| (239, 197, 183), // #EFC5B7 | ||
| (243, 210, 199), // #F3D2C7 | ||
| ]; | ||
|
|
||
| const OPENAI_SHADES: [(u8, u8, u8); 7] = [ | ||
| (16, 185, 129), // #10B981 | ||
| (18, 208, 145), // #12D091 | ||
| (20, 232, 162), // #14E8A2 | ||
| (41, 236, 172), // #29ECAC | ||
| (61, 238, 179), // #3DEEB3 | ||
| (97, 241, 193), // #61F1C1 | ||
| (133, 244, 208), // #85F4D0 | ||
| ]; | ||
|
|
||
| const GOOGLE_SHADES: [(u8, u8, u8); 7] = [ | ||
| (59, 130, 246), // #3B82F6 | ||
| (83, 146, 247), // #5392F7 | ||
| (108, 161, 248), // #6CA1F8 | ||
| (132, 177, 249), // #84B1F9 | ||
| (153, 190, 250), // #99BEFA | ||
| (172, 202, 251), // #ACCAFB | ||
| (190, 214, 252), // #BED6FC | ||
| ]; | ||
|
|
||
| const DEEPSEEK_SHADES: [(u8, u8, u8); 7] = [ | ||
| (6, 182, 212), // #06B6D4 | ||
| (7, 203, 237), // #07CBED | ||
| (21, 215, 248), // #15D7F8 | ||
| (45, 219, 249), // #2DDBF9 | ||
| (66, 223, 250), // #42DFFA | ||
| (85, 226, 250), // #55E2FA | ||
| (105, 229, 251), // #69E5FB | ||
| ]; | ||
|
|
||
| const XAI_SHADES: [(u8, u8, u8); 7] = [ | ||
| (234, 179, 8), // #EAB308 | ||
| (247, 192, 21), // #F7C015 | ||
| (248, 199, 45), // #F8C72D | ||
| (249, 205, 70), // #F9CD46 | ||
| (249, 211, 91), // #F9D35B | ||
| (250, 216, 110), // #FAD86E | ||
| (251, 221, 129), // #FBDD81 | ||
| ]; | ||
|
|
||
| const META_SHADES: [(u8, u8, u8); 7] = [ | ||
| (99, 102, 241), // #6366F1 | ||
| (122, 125, 243), // #7A7DF3 | ||
| (146, 148, 245), // #9294F5 | ||
| (169, 171, 247), // #A9ABF7 | ||
| (189, 190, 249), // #BDBEF9 | ||
| (207, 208, 251), // #CFD0FB | ||
| (225, 226, 252), // #E1E2FC | ||
| ]; | ||
|
|
||
| /// Neutral gray ramp for providers that don't match any known palette. | ||
| /// Still produces distinct shades per rank instead of collapsing to white. | ||
| const UNKNOWN_SHADES: [(u8, u8, u8); 7] = [ | ||
| (136, 136, 136), // #888888 | ||
| (156, 156, 156), // #9C9C9C | ||
| (176, 176, 176), // #B0B0B0 | ||
| (196, 196, 196), // #C4C4C4 | ||
| (212, 212, 212), // #D4D4D4 | ||
| (228, 228, 228), // #E4E4E4 | ||
| (244, 244, 244), // #F4F4F4 | ||
| ]; | ||
| const UNKNOWN_CLIENT_COLOR: Color = Color::Rgb(136, 136, 136); | ||
|
|
||
| pub fn provider_color_key(provider: &str) -> &str { | ||
| provider | ||
| .split(", ") | ||
| .map(str::trim) | ||
| .find(|segment| !segment.is_empty()) | ||
| .unwrap_or("unknown") | ||
| Color::Rgb(component(0..2), component(2..4), component(4..6)) | ||
| } | ||
|
|
||
| pub fn get_client_color(client: &str) -> Color { | ||
| let client_key = client.trim().to_lowercase(); | ||
| if let Some(color) = TokscaleConfig::initialized() | ||
| .get_client_color_hex(&client_key) | ||
| .and_then(parse_hex_color) | ||
| { | ||
| return color; | ||
| } | ||
| let Some(client_id) = ClientId::from_str(&client_key) else { | ||
| return UNKNOWN_CLIENT_COLOR; | ||
| }; | ||
| parse_catalog_color(client_id.color()) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
目录颜色解析从“优雅降级”退化为运行时 panic
parse_catalog_color 现在通过 .expect()/assert_eq! 强校验 #RRGGBB 格式;一旦 client-catalog.json 中某个客户端的颜色字段格式异常(缺 #、长度不对、非十六进制字符),get_client_color 对已知 ClientId 的调用会直接 panic,而不是像未知客户端那样回退到 UNKNOWN_CLIENT_COLOR。根据变更说明,旧实现是基于 parse_hex_color 的 Option 优雅降级流程,本次改动主动移除了这层保护。
crates/tokscale-core/build.rs 的 generate_rust 只是把 JSON 里的 color 字符串原样写入常量,并未在编译期校验格式;目前唯一的安全网是 every_catalog_client_color_is_parseable 这一单测。一旦未来目录条目(尤其是通过 fork/上游合并流程新增的客户端)颜色字段有误且未被及时测试捕获,会导致整个 TUI 在渲染阶段崩溃。
建议恢复优雅降级,对解析失败返回中性色而非 panic:
🛡️ 建议修复:将解析失败降级为中性色而非 panic
-fn parse_catalog_color(hex: &str) -> Color {
- let hex = hex
- .strip_prefix('#')
- .expect("client catalog colors are validated as `#RRGGBB`");
- assert_eq!(
- hex.len(),
- 6,
- "client catalog colors are validated as `#RRGGBB`"
- );
- let component = |range| {
- u8::from_str_radix(&hex[range], 16).expect("client catalog colors are validated as `#RRGGBB`")
- };
- Color::Rgb(component(0..2), component(2..4), component(4..6))
-}
-
-pub fn get_client_color(client: &str) -> Color {
- let client_key = client.trim().to_lowercase();
- let Some(client_id) = ClientId::from_str(&client_key) else {
- return UNKNOWN_CLIENT_COLOR;
- };
- parse_catalog_color(client_id.color())
-}
+fn parse_catalog_color(hex: &str) -> Option<Color> {
+ let hex = hex.strip_prefix('#')?;
+ if hex.len() != 6 {
+ return None;
+ }
+ let component = |range: std::ops::Range<usize>| u8::from_str_radix(hex.get(range)?, 16).ok();
+ Some(Color::Rgb(component(0..2)?, component(2..4)?, component(4..6)?))
+}
+
+pub fn get_client_color(client: &str) -> Color {
+ let client_key = client.trim().to_lowercase();
+ let Some(client_id) = ClientId::from_str(&client_key) else {
+ return UNKNOWN_CLIENT_COLOR;
+ };
+ parse_catalog_color(client_id.color()).unwrap_or(UNKNOWN_CLIENT_COLOR)
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn parse_catalog_color(hex: &str) -> Color { | |
| let hex = hex | |
| .strip_prefix('#') | |
| .expect("client catalog colors are validated as #RRGGBB"); | |
| assert_eq!( | |
| hex.len(), | |
| 6, | |
| "client catalog colors are validated as #RRGGBB" | |
| ); | |
| let component = |range| { | |
| u8::from_str_radix(&hex[range], 16).expect("client catalog colors are validated as #RRGGBB") | |
| }; | |
| Color::Rgb(lerp(r), lerp(g), lerp(b)) | |
| } | |
| const ANTHROPIC_SHADES: [(u8, u8, u8); 7] = [ | |
| (218, 119, 86), // #DA7756 | |
| (223, 136, 107), // #DF886B | |
| (227, 153, 128), // #E39980 | |
| (232, 170, 149), // #E8AA95 | |
| (236, 184, 166), // #ECB8A6 | |
| (239, 197, 183), // #EFC5B7 | |
| (243, 210, 199), // #F3D2C7 | |
| ]; | |
| const OPENAI_SHADES: [(u8, u8, u8); 7] = [ | |
| (16, 185, 129), // #10B981 | |
| (18, 208, 145), // #12D091 | |
| (20, 232, 162), // #14E8A2 | |
| (41, 236, 172), // #29ECAC | |
| (61, 238, 179), // #3DEEB3 | |
| (97, 241, 193), // #61F1C1 | |
| (133, 244, 208), // #85F4D0 | |
| ]; | |
| const GOOGLE_SHADES: [(u8, u8, u8); 7] = [ | |
| (59, 130, 246), // #3B82F6 | |
| (83, 146, 247), // #5392F7 | |
| (108, 161, 248), // #6CA1F8 | |
| (132, 177, 249), // #84B1F9 | |
| (153, 190, 250), // #99BEFA | |
| (172, 202, 251), // #ACCAFB | |
| (190, 214, 252), // #BED6FC | |
| ]; | |
| const DEEPSEEK_SHADES: [(u8, u8, u8); 7] = [ | |
| (6, 182, 212), // #06B6D4 | |
| (7, 203, 237), // #07CBED | |
| (21, 215, 248), // #15D7F8 | |
| (45, 219, 249), // #2DDBF9 | |
| (66, 223, 250), // #42DFFA | |
| (85, 226, 250), // #55E2FA | |
| (105, 229, 251), // #69E5FB | |
| ]; | |
| const XAI_SHADES: [(u8, u8, u8); 7] = [ | |
| (234, 179, 8), // #EAB308 | |
| (247, 192, 21), // #F7C015 | |
| (248, 199, 45), // #F8C72D | |
| (249, 205, 70), // #F9CD46 | |
| (249, 211, 91), // #F9D35B | |
| (250, 216, 110), // #FAD86E | |
| (251, 221, 129), // #FBDD81 | |
| ]; | |
| const META_SHADES: [(u8, u8, u8); 7] = [ | |
| (99, 102, 241), // #6366F1 | |
| (122, 125, 243), // #7A7DF3 | |
| (146, 148, 245), // #9294F5 | |
| (169, 171, 247), // #A9ABF7 | |
| (189, 190, 249), // #BDBEF9 | |
| (207, 208, 251), // #CFD0FB | |
| (225, 226, 252), // #E1E2FC | |
| ]; | |
| /// Neutral gray ramp for providers that don't match any known palette. | |
| /// Still produces distinct shades per rank instead of collapsing to white. | |
| const UNKNOWN_SHADES: [(u8, u8, u8); 7] = [ | |
| (136, 136, 136), // #888888 | |
| (156, 156, 156), // #9C9C9C | |
| (176, 176, 176), // #B0B0B0 | |
| (196, 196, 196), // #C4C4C4 | |
| (212, 212, 212), // #D4D4D4 | |
| (228, 228, 228), // #E4E4E4 | |
| (244, 244, 244), // #F4F4F4 | |
| ]; | |
| const UNKNOWN_CLIENT_COLOR: Color = Color::Rgb(136, 136, 136); | |
| pub fn provider_color_key(provider: &str) -> &str { | |
| provider | |
| .split(", ") | |
| .map(str::trim) | |
| .find(|segment| !segment.is_empty()) | |
| .unwrap_or("unknown") | |
| Color::Rgb(component(0..2), component(2..4), component(4..6)) | |
| } | |
| pub fn get_client_color(client: &str) -> Color { | |
| let client_key = client.trim().to_lowercase(); | |
| if let Some(color) = TokscaleConfig::initialized() | |
| .get_client_color_hex(&client_key) | |
| .and_then(parse_hex_color) | |
| { | |
| return color; | |
| } | |
| let Some(client_id) = ClientId::from_str(&client_key) else { | |
| return UNKNOWN_CLIENT_COLOR; | |
| }; | |
| parse_catalog_color(client_id.color()) | |
| } | |
| fn parse_catalog_color(hex: &str) -> Option<Color> { | |
| let hex = hex.strip_prefix('#')?; | |
| if hex.len() != 6 { | |
| return None; | |
| } | |
| let component = |range: std::ops::Range<usize>| u8::from_str_radix(hex.get(range)?, 16).ok(); | |
| Some(Color::Rgb(component(0..2)?, component(2..4)?, component(4..6)?)) | |
| } | |
| pub fn get_client_color(client: &str) -> Color { | |
| let client_key = client.trim().to_lowercase(); | |
| let Some(client_id) = ClientId::from_str(&client_key) else { | |
| return UNKNOWN_CLIENT_COLOR; | |
| }; | |
| parse_catalog_color(client_id.color()).unwrap_or(UNKNOWN_CLIENT_COLOR) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/tokscale-cli/src/tui/colors.rs` around lines 47 - 68, Update
parse_catalog_color to return an optional result instead of panicking when the
color lacks the # prefix, has an invalid length, or contains non-hexadecimal
characters. In get_client_color, fall back to UNKNOWN_CLIENT_COLOR when parsing
the known ClientId color fails, while preserving the existing unknown-client
fallback.
Summary
*inversion, and explicit validation before committing a non-empty selection.Testing
cargo fmt --all -- --checkcargo test -p tokscale-cli(897 passed, 1 ignored)Summary by CodeRabbit
新功能
改进
缓存