feat: restore quota panel for OpenCode (Go Dzen) + code quality improvements - #5
Conversation
Closes #4 - Restore quota panel adapted for OpenCode with Go Dzen subscription - Add GPLv3 LICENSE file - Add proper .gitignore for Rust project - Add cross-platform CI matrix (ubuntu/macos/windows) - Add README badges (CI, License, Rust) - Remove dead code (~250 lines): DesktopRolloutScanner, codex_enabled - Eliminate code duplication: shared truncate_at_char_boundary - Fix auto_discover.rs effort field hack - Atomic config write (tmp + rename pattern) - Kill logic: SIGTERM → 3s wait → SIGKILL - Summary cache bounded to 500 entries - Add OpenCode rate limit reader Verification: - cargo check ✅ - cargo clippy -- -D warnings ✅ - cargo test ✅ (88/88 passed) - cargo build --release ✅
There was a problem hiding this comment.
Code Review
This pull request introduces a new Quota panel to track OpenCode rate limits, refactors string truncation logic, removes the unused desktop rollout scanner, and updates the session kill sequence to gracefully terminate processes. The feedback highlights several critical improvements: running the blocking kill-and-sleep sequence in a background thread to prevent TUI freezes, resolving OpenCode config paths on Windows for cross-platform compatibility, optimizing the summary cache eviction loop to avoid performance degradation, and scaling token rate calculations proportionally during startup when history is limited.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let _ = std::process::Command::new("kill") | ||
| .args(["-9", &pid.to_string()]) | ||
| .args(["-15", &pid.to_string()]) | ||
| .output(); | ||
| std::thread::sleep(std::time::Duration::from_secs(3)); | ||
| if std::process::Command::new("kill") | ||
| .args(["-0", &pid.to_string()]) | ||
| .status() | ||
| .is_ok_and(|s| s.success()) | ||
| { | ||
| let _ = std::process::Command::new("kill") | ||
| .args(["-9", &pid.to_string()]) | ||
| .output(); | ||
| } |
There was a problem hiding this comment.
Blocking the main thread with std::thread::sleep(std::time::Duration::from_secs(3)) in a TUI application will freeze the entire user interface for 3 seconds whenever a session is killed.
To keep the TUI responsive, the graceful kill sequence (sending SIGTERM, waiting, and then sending SIGKILL if still alive) should be executed in a background thread.
| let _ = std::process::Command::new("kill") | |
| .args(["-9", &pid.to_string()]) | |
| .args(["-15", &pid.to_string()]) | |
| .output(); | |
| std::thread::sleep(std::time::Duration::from_secs(3)); | |
| if std::process::Command::new("kill") | |
| .args(["-0", &pid.to_string()]) | |
| .status() | |
| .is_ok_and(|s| s.success()) | |
| { | |
| let _ = std::process::Command::new("kill") | |
| .args(["-9", &pid.to_string()]) | |
| .output(); | |
| } | |
| std::thread::spawn(move || { | |
| let _ = std::process::Command::new("kill") | |
| .args(["-15", &pid.to_string()]) | |
| .output(); | |
| std::thread::sleep(std::time::Duration::from_secs(3)); | |
| if std::process::Command::new("kill") | |
| .args(["-0", &pid.to_string()]) | |
| .status() | |
| .is_ok_and(|s| s.success()) | |
| { | |
| let _ = std::process::Command::new("kill") | |
| .args(["-9", &pid.to_string()]) | |
| .output(); | |
| } | |
| }); |
| pub fn read_opencode_rate_limits() -> Vec<RateLimitInfo> { | ||
| let mut results = Vec::new(); | ||
| let Some(home) = dirs::home_dir() else { | ||
| return results; | ||
| }; | ||
| let path = home.join(".config").join("opencode").join(OPENCODE_RATE_FILE); | ||
| if let Some(info) = read_rate_file(&path, "opencode") { | ||
| results.push(info); | ||
| } | ||
| results | ||
| } |
There was a problem hiding this comment.
On Windows, OpenCode configurations and rate limit files are typically stored in %LOCALAPPDATA%/opencode or %APPDATA%/opencode rather than ~/.config/opencode. The current implementation only checks the Unix-style ~/.config path, which will cause the quota panel to show "no data" on Windows.
To ensure cross-platform compatibility, we should check %LOCALAPPDATA% and %APPDATA% on Windows, matching the path resolution logic used in opencode.rs.
pub fn read_opencode_rate_limits() -> Vec<RateLimitInfo> {
let mut results = Vec::new();
let Some(home) = dirs::home_dir() else {
return results;
};
let mut path = home.join(".config").join("opencode").join(OPENCODE_RATE_FILE);
#[cfg(target_os = "windows")]
if !path.exists() {
for var in ["LOCALAPPDATA", "APPDATA"] {
if let Ok(base) = std::env::var(var) {
if !base.is_empty() {
let candidate = std::path::PathBuf::from(base).join("opencode").join(OPENCODE_RATE_FILE);
if candidate.exists() {
path = candidate;
break;
}
}
}
}
}
if let Some(info) = read_rate_file(&path, "opencode") {
results.push(info);
}
results
}| while bounded.len() > MAX_CACHED_SUMMARIES { | ||
| if let Some(oldest_key) = bounded.keys().next().cloned() { | ||
| bounded.remove(&oldest_key); | ||
| } else { | ||
| break; | ||
| } | ||
| } |
There was a problem hiding this comment.
Calling bounded.keys().next() repeatedly in a loop to evict elements is highly inefficient. Each call to .keys() creates a new iterator that must scan the internal bucket array of the HashMap from the beginning to find the first occupied bucket. If many elements need to be evicted, this results in
Additionally, HashMap has no defined order, so keys().next() does not return the "oldest" key but rather an arbitrary one.
We can optimize this to
if bounded.len() > MAX_CACHED_SUMMARIES {
let keys_to_remove: Vec<String> = bounded.keys()
.take(bounded.len() - MAX_CACHED_SUMMARIES)
.cloned()
.collect();
for key in keys_to_remove {
bounded.remove(&key);
}
}| let rates = &app.token_rates; | ||
| let ticks_per_min: usize = 30; | ||
| let tokens_per_min: f64 = rates.iter().rev().take(ticks_per_min).sum(); |
There was a problem hiding this comment.
Right after application startup, app.token_rates will have fewer than 30 elements (ticks). Summing them directly as tokens_per_min will result in an artificially low rate that slowly climbs over the first minute, even if the agent is actively processing tokens at a constant rate.
To provide an accurate rate immediately, we should scale the sum proportionally when there are fewer than 30 ticks of history available.
let rates = &app.token_rates;
let ticks_per_min: usize = 30;
let rates_taken = rates.iter().rev().take(ticks_per_min).collect::<Vec<_>>();
let sum_tokens: f64 = rates_taken.iter().copied().copied().sum();
let tokens_per_min = if !rates_taken.is_empty() {
sum_tokens * (ticks_per_min as f64 / rates_taken.len() as f64)
} else {
0.0
};- Add GPU monitoring via nvml-wrapper (optional 'nvidia' feature flag) - New src/gpu.rs: GpuMetrics struct + GpuSampler with conditional compilation - New src/ui/gpu.rs: GPU panel with temp/utilization/VRAM/power display - Replace sqlite3 CLI subprocess with native rusqlite (bundled feature) - Remove sqlite3 availability checks and Windows warnings - Fix clippy manual_flatten warnings in opencode.rs queries - Add GPU panel toggle (key 8) in view menu and config overlay - Full i18n support for GPU panel (EN + ZH) Verification: - cargo check ✅ - cargo check --features nvidia ✅ - cargo clippy -- -D warnings ✅ - cargo test ✅ (88/88 passed)
- Parallel port probing in auto_discover.rs using std::thread::scope (chunks of 10 concurrent threads, avoids sequential 300ms timeouts) - Threshold alerts: context > 90%, rate limits > 80%, orphan ports - Alerts shown as transient status messages in tick_no_summaries() Verification: - cargo clippy -- -D warnings ✅ - cargo test ✅ (88/88 passed)
- Add deny.toml with license/advisory/ban/source policies - Add cargo-deny CI step (EmbarkStudios/cargo-deny-action@v2) - Split src/ui/sessions.rs (52KB monolith) into: - sessions/mod.rs: main draw functions + tests - sessions/timeline.rs: timeline rendering - sessions/file_audit.rs: file audit rendering - sessions/detail.rs: chat history rendering Verification: - cargo clippy -- -D warnings ✅ - cargo test ✅ (88/88 passed)
Summary
Closes #4
Comprehensive code quality improvements, quota panel restoration for OpenCode (Go Dzen), GPU monitoring, and architectural refactoring.
Commits (4)
1. Quota Panel + Code Quality
~/.config/opencode/llmtop-rate-limits.json2. GPU Monitoring + rusqlite
nvml-wrapper(optionalnvidiafeature flag)rusqlite(bundled) — no external binary neededmanual_flattenwarnings3. Parallel Probing + Alerts
4. cargo-deny + Module Split
deny.tomlwith license/advisory/ban policiessessions.rs(52KB) intosessions/{mod,timeline,file_audit,detail}.rsVerification
Files Changed (25+)