Reads ~/.claude/projects/*.jsonl transcripts from the open WSL panes'
distros and shows per-session token counts + estimated USD cost, with a
running total in the titlebar.
Backend (src-tauri/src/usage.rs): new get_claude_usage command. For each
distro it probes $HOME once via wsl.exe, reaches the transcripts over the
\\wsl.localhost UNC share, and tallies message.usage per model per
session (summed by each line's model, since a session can switch models).
Results cached by (path,size,mtime) so polling only re-parses the file
that grew; recency-capped (30d / 50 sessions) to bound scan cost.
Windows-only; returns [] elsewhere. quiet_command made pub(crate).
Frontend: src/lib/usage.ts holds the pricing table (per-MTok rates,
matched by model-family substring) + cost/format helpers, so rates are
editable without recompiling Rust. UsagePanel.tsx mirrors the MCP panel
modal; rows whose transcript cwd matches an open pane are highlighted
with a [pane: label] tag. App polls every 20s (visible windows) for the
titlebar 💰 total and every 5s while the panel is open. Ctrl+Shift+U
opens it; added to shortcuts.ts + regenerated README.
tsc clean. Rust builds on the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
4.9 KiB
Rust
117 lines
4.9 KiB
Rust
//! Library entry point. `main.rs` calls `run()`.
|
|
|
|
mod commands;
|
|
mod creds;
|
|
mod hosts;
|
|
mod mcp;
|
|
mod mcp_policy;
|
|
mod pty;
|
|
mod usage;
|
|
mod window_state;
|
|
|
|
use std::sync::Arc;
|
|
|
|
// `Manager` trait must be in scope to call `.app_handle()` on the `&Window`
|
|
// passed to the `on_window_event` closure below. Same pattern as the
|
|
// `Emitter` trait needed for `.emit()` (see 2026-05-26 PR-1 session log).
|
|
use tauri::Manager;
|
|
|
|
use crate::mcp::{McpServerHandle, McpState, PendingActions};
|
|
use crate::pty::PtyManager;
|
|
use crate::window_state::{PendingInits, WindowsState, MAIN_WINDOW_LABEL};
|
|
|
|
pub fn run() {
|
|
let _ = tracing_subscriber::fmt()
|
|
.with_env_filter(
|
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
|
)
|
|
.with_writer(std::io::stderr)
|
|
.try_init();
|
|
|
|
// keyring-core 1.x requires explicit store registration before any
|
|
// Entry::new() call. We're Windows-only so the Credential Manager
|
|
// backend is the only choice. Failure here means SSH passwords won't
|
|
// be retrievable — log and continue (host configs still work without
|
|
// saved passwords; users just see the prompt and type it manually).
|
|
match windows_native_keyring_store::Store::new() {
|
|
Ok(store) => keyring_core::set_default_store(store),
|
|
Err(e) => tracing::warn!("keyring store init failed: {e}"),
|
|
}
|
|
|
|
// PtyManager and McpState are shared with the MCP server, so register
|
|
// them as Arc<T> rather than the plain T. Tauri commands access them
|
|
// via `tauri::State<'_, Arc<T>>` and deref / clone as needed.
|
|
let ptys: Arc<PtyManager> = Arc::new(PtyManager::new());
|
|
let mcp_state: Arc<tokio::sync::RwLock<McpState>> =
|
|
Arc::new(tokio::sync::RwLock::new(McpState::default()));
|
|
// Pending action registry — separate managed state so mcp_action_reply can
|
|
// grab it without needing to lock McpState or reach into TileService.
|
|
let pending_actions: Arc<PendingActions> = Arc::new(PendingActions::default());
|
|
// Cross-window workspace aggregator: every window pushes its tab list
|
|
// here; backend debounces + writes the merged envelope to workspace.json.
|
|
let windows_state: Arc<WindowsState> = Arc::new(WindowsState::default());
|
|
// Pane-transfer pending-init registry: source window stashes a payload
|
|
// keyed by the new window's label; target window pulls it during mount.
|
|
let pending_inits: Arc<PendingInits> = Arc::new(PendingInits::default());
|
|
|
|
let windows_state_for_event = Arc::clone(&windows_state);
|
|
let pending_inits_for_event = Arc::clone(&pending_inits);
|
|
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.manage(ptys)
|
|
.manage(mcp_state)
|
|
.manage(McpServerHandle::default())
|
|
.manage(pending_actions)
|
|
.manage(windows_state)
|
|
.manage(pending_inits)
|
|
.manage(usage::UsageCache::default())
|
|
.on_window_event(move |window, event| {
|
|
// When a non-main window closes, drop its workspaces from the
|
|
// aggregator AND any unconsumed pending-init payload so neither
|
|
// resurrect on next launch. Matches Chrome-style "closing a
|
|
// detached window discards its tabs" intent.
|
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
|
let label = window.label().to_string();
|
|
if label != MAIN_WINDOW_LABEL {
|
|
pending_inits_for_event.by_label.lock().remove(&label);
|
|
windows_state_for_event
|
|
.forget(window.app_handle().clone(), &label);
|
|
}
|
|
}
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::list_distros,
|
|
commands::spawn_pane,
|
|
commands::write_to_pane,
|
|
commands::resize_pane,
|
|
commands::kill_pane,
|
|
commands::mark_pane_transferring,
|
|
commands::claim_pane,
|
|
commands::get_pane_ring,
|
|
commands::create_pane_window,
|
|
commands::take_pending_window_init,
|
|
commands::push_window_workspaces,
|
|
commands::save_workspace,
|
|
commands::load_workspace,
|
|
commands::list_ssh_hosts,
|
|
commands::save_ssh_hosts,
|
|
commands::set_host_password,
|
|
commands::delete_host_password,
|
|
commands::has_host_password,
|
|
commands::mcp_start,
|
|
commands::mcp_stop,
|
|
commands::mcp_status,
|
|
commands::mcp_regenerate_token,
|
|
commands::mcp_update_state,
|
|
commands::mcp_action_reply,
|
|
commands::mcp_policy_load,
|
|
commands::mcp_policy_save,
|
|
commands::mcp_hard_deny_labels,
|
|
usage::get_claude_usage,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|