Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.

feat(tui): streamline client filtering and model-family presentation - #187

Merged
makoMakoGo merged 5 commits into
personal/local-clientsfrom
codex/tui-client-picker-snapshot
Jul 22, 2026
Merged

feat(tui): streamline client filtering and model-family presentation#187
makoMakoGo merged 5 commits into
personal/local-clientsfrom
codex/tui-client-picker-snapshot

Conversation

@makoMakoGo

@makoMakoGo makoMakoGo commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace per-client picker hotkeys with name filtering, search-scoped * inversion, and explicit validation before committing a non-empty selection.
  • Derive known-model colors and favorite-model presentation from canonical model families, removing provider-dependent color attribution and redundant cache/config state.
  • Keep locked achievements neutral and refine the Snapshot labels, portraits, empty state, diagnostic grouping, and height-aware spacing.
  • Bump the TUI cache schema and update the client-selection and model-identity contracts to match the simplified data flow.

Testing

  • cargo fmt --all -- --check
  • cargo test -p tokscale-cli (897 passed, 1 ignored)

Summary by CodeRabbit

  • 新功能

    • 模型颜色现根据模型家族稳定显示,未知模型使用中性色。
    • 客户端选择器支持使用“*”反选当前筛选结果,并提供更清晰的空选择提示。
    • 概览面板新增“扫描会话数”指标,并优化紧凑布局与收藏客户端展示。
  • 改进

    • 移除客户端快捷键交互,改为直接输入名称并使用空格切换。
    • 模型详情与统计视图的身份和颜色显示更加一致。
  • 缓存

    • 缓存格式升级,旧缓存将自动重建。

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

PR 将 TUI 模型身份统一为 model_id,以模型家族固定色替代 provider shade map;同时删除 color_key 与客户端热键,升级缓存 schema 至 44,并调整客户端选择器及 Overview Snapshot 布局。

Changes

模型身份与颜色

Layer / File(s) Summary
Canonical model identity and family colors
crates/tokscale-core/src/usage_views.rs, crates/tokscale-cli/src/tui/model_family.rs, crates/tokscale-cli/src/tui/colors.rs, crates/tokscale-cli/src/tui/app.rs, crates/tokscale-cli/src/tui/ui/*
模型视图移除 color_key,详情行保留 canonical model_id,颜色按 ModelFamily 固定映射,并应用到各模型、概览、统计和肖像渲染路径。

缓存 schema 44

Layer / File(s) Summary
Schema 44 cache projection
crates/tokscale-cli/src/tui/cache.rs, docs/adr/0008-single-copy-memory-pipeline.md, docs/adr/0028-client-universe-and-view-selection.md
缓存 schema 从 43 升级到 44,序列化模型信息不再包含 colorKey,旧 schema 加载改为显式 miss。

客户端目录与选择器

Layer / File(s) Summary
Client catalog and picker interaction
crates/tokscale-core/build.rs, crates/tokscale-core/client-catalog.json, crates/tokscale-cli/src/tui/ui/dialog/client_picker.rs, crates/tokscale-cli/src/tui/config.rs
客户端目录及生成 API 移除热键;选择器支持过滤结果反选、空草稿提交校验和宽度自适应提示。

概览布局

Layer / File(s) Summary
Overview snapshot and achievement rendering
crates/tokscale-cli/src/tui/ui/overview_snapshot.rs, crates/tokscale-cli/src/tui/ui/achievements.rs
Core 区新增 Sessions Scanned,诊断信息按高度追加,Fun 空态与 Favorite Client 布局调整,成就渲染改为接收 Theme。

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: 提交非空选择
Loading

Possibly related issues

Possibly related PRs

Poem

小兔挥爪删色键,
模型身份归本源。
家族品牌添新彩,
热键随风不复还。
缓存四四稳稳眠。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确概括了本次 PR 的核心变更:客户端筛选简化与模型家族展示/着色调整。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/tui-client-picker-snapshot

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 43 untouched benchmarks


Comparing codex/tui-client-picker-snapshot (07038ea) with personal/local-clients (a3c890b)

Open in CodSpeed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between a3c890b and 07038ea.

📒 Files selected for processing (32)
  • crates/tokscale-cli/src/tui/app.rs
  • crates/tokscale-cli/src/tui/cache.rs
  • crates/tokscale-cli/src/tui/colors.rs
  • crates/tokscale-cli/src/tui/config.rs
  • crates/tokscale-cli/src/tui/data/mod.rs
  • crates/tokscale-cli/src/tui/data/overview.rs
  • crates/tokscale-cli/src/tui/mod.rs
  • crates/tokscale-cli/src/tui/model_family.rs
  • crates/tokscale-cli/src/tui/ui/achievements.rs
  • crates/tokscale-cli/src/tui/ui/daily.rs
  • crates/tokscale-cli/src/tui/ui/dialog/client_picker.rs
  • crates/tokscale-cli/src/tui/ui/hourly.rs
  • crates/tokscale-cli/src/tui/ui/mod.rs
  • crates/tokscale-cli/src/tui/ui/models.rs
  • crates/tokscale-cli/src/tui/ui/overview.rs
  • crates/tokscale-cli/src/tui/ui/overview_snapshot.rs
  • crates/tokscale-cli/src/tui/ui/period.rs
  • crates/tokscale-cli/src/tui/ui/portraits.rs
  • crates/tokscale-cli/src/tui/ui/stats.rs
  • crates/tokscale-core/build.rs
  • crates/tokscale-core/client-catalog.json
  • crates/tokscale-core/src/aggregate/tui.rs
  • crates/tokscale-core/src/clients.rs
  • crates/tokscale-core/src/provider_identity.rs
  • crates/tokscale-core/src/usage_views.rs
  • docs/adr/0007-client-identity-catalog.md
  • docs/adr/0008-single-copy-memory-pipeline.md
  • docs/adr/0012-local-client-exclusions.md
  • docs/adr/0026-group-by-view-scope-contract.md
  • docs/adr/0028-tui-client-universe-and-view-selection.md
  • docs/adr/0029-keep-claude-discovery-catalog-scoped.md
  • docs/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

Comment on lines +47 to 68
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())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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_colorOption 优雅降级流程,本次改动主动移除了这层保护。

crates/tokscale-core/build.rsgenerate_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.

Suggested change
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.

@makoMakoGo
makoMakoGo merged commit 07038ea into personal/local-clients Jul 22, 2026
9 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant