Skip to content
This repository was archived by the owner on Jul 30, 2026. It is now read-only.
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
15 changes: 14 additions & 1 deletion .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
bin_name: tokscale
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
build: cargo build --release -p tokscale-cli --target x86_64-unknown-linux-gnu
build: cargo zigbuild --release -p tokscale-cli --target x86_64-unknown-linux-gnu
strip: strip target/x86_64-unknown-linux-gnu/release/tokscale
bin_name: tokscale

Expand All @@ -54,6 +54,19 @@ jobs:
target/
key: cli-${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}-${{ hashFiles('Cargo.lock') }}

- uses: mlugg/setup-zig@v2
if: ${{ contains(matrix.settings.target, 'linux') }}
with:
version: 0.13.0

- name: Install cargo-zigbuild
uses: taiki-e/install-action@v2
if: ${{ contains(matrix.settings.target, 'linux') }}
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tool: cargo-zigbuild

- name: Build CLI binary
run: ${{ matrix.settings.build }}
shell: bash
Expand Down
96 changes: 92 additions & 4 deletions crates/tokscale-cli/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ pub struct App {

pub background_loading: bool,

pub blocking_loading: bool,

pub needs_reload: bool,

pub dialog_stack: DialogStack,
Expand Down Expand Up @@ -342,6 +344,7 @@ impl App {
click_areas: Vec::new(),
spinner_frame: 0,
background_loading: false,
blocking_loading: false,
needs_reload: false,
dialog_stack,
dialog_needs_reload,
Expand Down Expand Up @@ -370,9 +373,38 @@ impl App {

pub fn set_background_loading(&mut self, loading: bool) {
self.background_loading = loading;
if !loading {
self.blocking_loading = false;
}
// Don't set data.loading - let cached data remain visible during background refresh
}

pub fn request_blocking_reload(&mut self) {
self.needs_reload = true;
self.blocking_loading = true;
}

pub fn is_blocking_loading(&self) -> bool {
self.blocking_loading
|| (!self.dialog_stack.is_active() && *self.dialog_needs_reload.borrow())
}

fn consume_dialog_reload_if_ready(&mut self) {
let needs_blocking_reload = {
let mut needs_reload = self.dialog_needs_reload.borrow_mut();
if !self.dialog_stack.is_active() && *needs_reload {
*needs_reload = false;
true
} else {
false
}
};

if needs_blocking_reload {
self.request_blocking_reload();
}
}

pub fn update_data(&mut self, data: UsageData) {
self.data = data;
self.data_version = self.data_version.saturating_add(1);
Expand Down Expand Up @@ -455,10 +487,7 @@ impl App {
self.needs_reload = true;
}

if *self.dialog_needs_reload.borrow() {
*self.dialog_needs_reload.borrow_mut() = false;
self.needs_reload = true;
}
self.consume_dialog_reload_if_ready();

// Poll background usage fetch
if let Some(ref rx) = self.usage_rx {
Expand Down Expand Up @@ -493,6 +522,7 @@ impl App {

if self.dialog_stack.is_active() {
self.dialog_stack.handle_key(key.code);
self.consume_dialog_reload_if_ready();
return false;
}

Expand Down Expand Up @@ -643,6 +673,7 @@ impl App {
pub fn handle_mouse_event(&mut self, event: MouseEvent) {
if self.dialog_stack.is_active() {
self.dialog_stack.handle_mouse(event);
self.consume_dialog_reload_if_ready();
return;
}

Expand Down Expand Up @@ -2948,6 +2979,63 @@ mod tests {
);
}

#[test]
fn test_group_by_change_requests_blocking_reload() {
let mut app = make_app();
*app.group_by.borrow_mut() = tokscale_core::GroupBy::ClientModel;

app.handle_key_event(key(KeyCode::Char('g')));
app.handle_key_event(key(KeyCode::Down));
app.handle_key_event(key(KeyCode::Down));
app.handle_key_event(key(KeyCode::Enter));

assert_eq!(
*app.group_by.borrow(),
tokscale_core::GroupBy::WorkspaceModel
);
assert!(!app.dialog_stack.is_active());
assert!(app.needs_reload);
assert!(app.blocking_loading);
assert!(app.is_blocking_loading());
}

#[test]
fn test_source_picker_waits_until_close_before_blocking_reload() {
let mut app = make_app();

app.handle_key_event(key(KeyCode::Char('s')));
assert!(app.dialog_stack.is_active());

app.handle_key_event(key(KeyCode::Enter));
assert!(app.dialog_stack.is_active());
assert!(!app.needs_reload);
assert!(!app.blocking_loading);
assert!(!app.is_blocking_loading());

app.on_tick();
assert!(!app.needs_reload);
assert!(!app.blocking_loading);

app.handle_key_event(key(KeyCode::Esc));

assert!(!app.dialog_stack.is_active());
assert!(app.needs_reload);
assert!(app.blocking_loading);
assert!(app.is_blocking_loading());
}

#[test]
fn test_background_loading_clear_resets_blocking_loading() {
let mut app = make_app();

app.request_blocking_reload();
app.set_background_loading(true);
app.set_background_loading(false);

assert!(!app.background_loading);
assert!(!app.blocking_loading);
}

// ── handle_key_event: misc keys ─────────────────────────────────

#[test]
Expand Down
6 changes: 3 additions & 3 deletions crates/tokscale-cli/src/tui/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use super::data::{

/// Cache staleness threshold: 5 minutes (matches TS implementation)
const CACHE_STALE_THRESHOLD_MS: u64 = 5 * 60 * 1000;
const CACHE_SCHEMA_VERSION: u32 = 12;
const CACHE_SCHEMA_VERSION: u32 = 13;

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -1012,7 +1012,7 @@ mod tests {
fs::write(
&cache_path,
r#"{
"schemaVersion": 12,
"schemaVersion": 13,
"timestamp": 9999999999999,
"enabledClients": ["claude"],
"includeSynthetic": false,
Expand Down Expand Up @@ -1281,7 +1281,7 @@ mod tests {
fs::create_dir_all(cache_path.parent().unwrap()).unwrap();
let mut cached: serde_json::Value = serde_json::from_str(
r#"{
"schemaVersion": 12,
"schemaVersion": 13,
"timestamp": 0,
"enabledClients": ["claude", "cursor"],
"includeSynthetic": false,
Expand Down
33 changes: 16 additions & 17 deletions crates/tokscale-cli/src/tui/ui/daily.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use ratatui::widgets::{

use super::model_usage_layout::{
display_width, model_usage_table_layout, ModelUsageColumn as DailyDetailColumn,
ModelUsageTableDensity as DailyDetailTableDensity,
ModelUsageLayoutProfile, ModelUsageTableDensity as DailyDetailTableDensity,
ModelUsageTableLayout as DailyDetailTableLayout, DETAIL_PROVIDER_WIDTH, DETAIL_SOURCE_WIDTH,
MODEL_MIN_WIDTH,
};
Expand Down Expand Up @@ -46,6 +46,18 @@ enum DailyColumn {
Cost,
}

const DAILY_DETAIL_OPTIONAL_COLUMNS: [DailyDetailColumn; 9] = [
DailyDetailColumn::Cost,
DailyDetailColumn::Source,
DailyDetailColumn::Provider,
DailyDetailColumn::Messages,
DailyDetailColumn::Input,
DailyDetailColumn::Output,
DailyDetailColumn::CacheRate,
DailyDetailColumn::CacheRead,
DailyDetailColumn::CacheWrite,
];

#[derive(Debug, Clone, PartialEq, Eq)]
struct DailyTableLayout {
columns: Vec<DailyColumn>,
Expand Down Expand Up @@ -208,16 +220,7 @@ fn daily_detail_table_layout(
model_content_width,
provider_content_width,
source_content_width,
&[
DailyDetailColumn::Source,
DailyDetailColumn::Provider,
DailyDetailColumn::Messages,
DailyDetailColumn::Input,
DailyDetailColumn::Output,
DailyDetailColumn::CacheRate,
DailyDetailColumn::CacheRead,
DailyDetailColumn::CacheWrite,
],
ModelUsageLayoutProfile::standard(&DAILY_DETAIL_OPTIONAL_COLUMNS),
)
}

Expand Down Expand Up @@ -755,17 +758,13 @@ mod tests {
}

#[test]
fn very_narrow_daily_detail_layout_keeps_model_tokens_and_cost() {
fn very_narrow_daily_detail_layout_keeps_model_and_tokens() {
let layout = daily_detail_table_layout(54, true, 80, 56, 40);

assert_eq!(layout.density, DailyDetailTableDensity::VeryCompact);
assert_eq!(
layout.columns,
vec![
DailyDetailColumn::Model,
DailyDetailColumn::Total,
DailyDetailColumn::Cost,
]
vec![DailyDetailColumn::Model, DailyDetailColumn::Total]
);
assert_eq!(layout.model_width, MODEL_MAX_WIDTH as usize);
}
Expand Down
2 changes: 1 addition & 1 deletion crates/tokscale-cli/src/tui/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub fn render(frame: &mut Frame, app: &mut App) {

header::render(frame, app, chunks[0]);

if app.data.loading && !app.background_loading {
if app.is_blocking_loading() || (app.data.loading && !app.background_loading) {
render_loading(frame, app, chunks[1]);
} else if let Some(ref error) = app.data.error {
render_error(frame, app, chunks[1], error);
Expand Down
Loading
Loading