fix(tui): make live session chrome truthful
Make the tool-details hint derive from the routed Alt/Option+V chord, so plain typing is never advertised as an action. Let a new composer draft take precedence over a stale completed turn, and keep elapsed time in the live tool row while the phase strip owns only the active tool count. Neutralize the cost heading and clarify the legacy DeepSeek base-URL route without changing provider behavior. Refresh every complete locale pack and add coverage for the shortcut, phase, timing, config, and cost contracts. Refs: #4716 #4717 #4718 #4719
This commit is contained in:
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "Cache telemetry — last {count} of {total} turn(s) (model: {model})\n",
|
||||
"CmdCacheNoData": "Cache history: no turns recorded yet.\n\n DeepSeek surfaces `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` on every API turn that the model supports it (V4 family). Run a turn and try /cache again.",
|
||||
"CmdCacheTotals": "Σ in: {sum_in} Σ hit: {sum_hit} Σ miss: {sum_miss} avg hit ratio: {avg}\n",
|
||||
"CmdCostReport": "Session Cost:\n ─────────────────────────────\n Approx total spent: {cost}\n\n Cost estimates are approximate and use provider usage telemetry when available.\n\n DeepSeek API Pricing:\n ─────────────────────────────\n Pricing details are not configured in this CLI.",
|
||||
"CmdCostReport": "Session Cost:\n ─────────────────────────────\n Approx total spent: {cost}\n\n Cost estimates are approximate and use provider usage telemetry when available.\n\n Provider API Pricing:\n ─────────────────────────────\n Pricing details are not configured in this CLI.",
|
||||
"CmdTokensCacheBoth": "{hit} hit / {miss} miss",
|
||||
"CmdTokensCacheHitOnly": "{hit} hit / miss not reported",
|
||||
"CmdTokensCacheMissOnly": "hit not reported / {miss} miss",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "Telemetría del caché — últimos {count} de {total} turno(s) (modelo: {model})\n",
|
||||
"CmdCacheNoData": "Historial del caché: ningún turno registrado todavía.\n\nDeepSeek expone `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` en cada turno de la API donde el modelo lo soporta (familia V4). Ejecuta un turno y prueba /cache de nuevo.",
|
||||
"CmdCacheTotals": "Σ entrada: {sum_in} Σ hit: {sum_hit} Σ miss: {sum_miss} tasa promedio de hit: {avg}\n",
|
||||
"CmdCostReport": "Costo de la sesión:\n─────────────────────────────\nTotal aproximado: {cost}\n\nLas estimaciones de costo son aproximadas y usan la telemetría de uso del proveedor cuando está disponible.\n\n Precios de la API DeepSeek:\n─────────────────────────────\nLos detalles de precio no están configurados en esta CLI.",
|
||||
"CmdCostReport": "Costo de la sesión:\n─────────────────────────────\nTotal aproximado: {cost}\n\nLas estimaciones de costo son aproximadas y usan la telemetría de uso del proveedor cuando está disponible.\n\n Precios de la API del proveedor:\n─────────────────────────────\nLos detalles de precio no están configurados en esta CLI.",
|
||||
"CmdTokensCacheBoth": "{hit} hit / {miss} miss",
|
||||
"CmdTokensCacheHitOnly": "{hit} hit / miss no reportado",
|
||||
"CmdTokensCacheMissOnly": "hit no reportado / {miss} miss",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "キャッシュテレメトリ — 直近 {count} / {total} ターン(モデル: {model})\n",
|
||||
"CmdCacheNoData": "キャッシュ履歴: まだターンを記録していません。\n\nDeepSeek は対応モデル (V4 系) の各 API ターンで `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` を返します。1 ターン実行してから /cache を再度試してください。",
|
||||
"CmdCacheTotals": "Σ 入力: {sum_in} Σ ヒット: {sum_hit} Σ ミス: {sum_miss} 平均ヒット率: {avg}\n",
|
||||
"CmdCostReport": "セッション費用:\n─────────────────────────────\n累計概算: {cost}\n\n費用は概算値。プロバイダの使用量テレメトリがあれば優先して使用します。\n\nDeepSeek API 料金:\n─────────────────────────────\n本 CLI には詳細な料金表は組み込まれていません。",
|
||||
"CmdCostReport": "セッション費用:\n─────────────────────────────\n累計概算: {cost}\n\n費用は概算値。プロバイダの使用量テレメトリがあれば優先して使用します。\n\nプロバイダー API 料金:\n─────────────────────────────\n本 CLI には詳細な料金表は組み込まれていません。",
|
||||
"CmdTokensCacheBoth": "ヒット {hit} / ミス {miss}",
|
||||
"CmdTokensCacheHitOnly": "ヒット {hit} / ミスは未報告",
|
||||
"CmdTokensCacheMissOnly": "ヒットは未報告 / ミス {miss}",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "캐시 텔레메트리 — 최근 {count}/{total}턴 (모델: {model})\n",
|
||||
"CmdCacheNoData": "캐시 기록: 아직 기록된 턴이 없습니다.\n\n DeepSeek은 모델이 지원하는 모든 API 턴(V4 계열)에서 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`를 제공합니다. 턴을 실행한 뒤 /cache를 다시 시도해보세요.",
|
||||
"CmdCacheTotals": "Σ 입력: {sum_in} Σ 적중: {sum_hit} Σ 실패: {sum_miss} 평균 적중률: {avg}\n",
|
||||
"CmdCostReport": "세션 비용:\n ─────────────────────────────\n 대략적인 총 지출: {cost}\n\n 비용 추정치는 근사값이며, 가능한 경우 프로바이더의 사용량 텔레메트리를 사용합니다.\n\n DeepSeek API 요금:\n ─────────────────────────────\n 이 CLI에는 요금 정보가 설정되어 있지 않습니다.",
|
||||
"CmdCostReport": "세션 비용:\n ─────────────────────────────\n 대략적인 총 지출: {cost}\n\n 비용 추정치는 근사값이며, 가능한 경우 프로바이더의 사용량 텔레메트리를 사용합니다.\n\n 프로바이더 API 요금:\n ─────────────────────────────\n 이 CLI에는 요금 정보가 설정되어 있지 않습니다.",
|
||||
"CmdTokensCacheBoth": "{hit} 적중 / {miss} 실패",
|
||||
"CmdTokensCacheHitOnly": "{hit} 적중 / 실패 미보고",
|
||||
"CmdTokensCacheMissOnly": "적중 미보고 / {miss} 실패",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "Telemetria do cache — últimos {count} de {total} turno(s) (modelo: {model})\n",
|
||||
"CmdCacheNoData": "Histórico do cache: nenhum turno registrado ainda.\n\nO DeepSeek expõe `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` em cada turno da API onde o modelo suporta (família V4). Execute um turno e tente /cache de novo.",
|
||||
"CmdCacheTotals": "Σ entrada: {sum_in} Σ hit: {sum_hit} Σ miss: {sum_miss} taxa média de hit: {avg}\n",
|
||||
"CmdCostReport": "Custo da sessão:\n─────────────────────────────\nTotal aproximado: {cost}\n\nEstimativas de custo são aproximadas e usam a telemetria de uso do provedor quando disponível.\n\nPreços da API DeepSeek:\n─────────────────────────────\nOs detalhes de preço não estão configurados nesta CLI.",
|
||||
"CmdCostReport": "Custo da sessão:\n─────────────────────────────\nTotal aproximado: {cost}\n\nEstimativas de custo são aproximadas e usam a telemetria de uso do provedor quando disponível.\n\nPreços da API do provedor:\n─────────────────────────────\nOs detalhes de preço não estão configurados nesta CLI.",
|
||||
"CmdTokensCacheBoth": "{hit} hit / {miss} miss",
|
||||
"CmdTokensCacheHitOnly": "{hit} hit / miss não reportado",
|
||||
"CmdTokensCacheMissOnly": "hit não reportado / {miss} miss",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "Thông tin cache — {count} lượt gần nhất trong tổng số {total} lượt (mô hình: {model})\n",
|
||||
"CmdCacheNoData": "Lịch sử bộ nhớ đệm: chưa có lượt nào được ghi nhận.\n\n DeepSeek cung cấp `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` trên mỗi lượt API mà mô hình hỗ trợ (dòng V4). Hãy chạy một lượt và thử lại lệnh /cache.",
|
||||
"CmdCacheTotals": "Σ vào: {sum_in} Σ hit: {sum_hit} Σ miss: {sum_miss} tỷ lệ hit trung bình: {avg}\n",
|
||||
"CmdCostReport": "Chi Phí Phiên Làm Việc:\n─────────────────────────────\nTổng chi tiêu ước tính: {cost}\n\nCác ước tính chi phí mang tính xấp xỉ và sử dụng dữ liệu viễn trắc từ nhà cung cấp nếu có.\n\nBảng Giá API DeepSeek:\n─────────────────────────────\nThông tin chi tiết về giá chưa được cấu hình trong CLI này.",
|
||||
"CmdCostReport": "Chi Phí Phiên Làm Việc:\n─────────────────────────────\nTổng chi tiêu ước tính: {cost}\n\nCác ước tính chi phí mang tính xấp xỉ và sử dụng dữ liệu viễn trắc từ nhà cung cấp nếu có.\n\nBảng giá API của nhà cung cấp:\n─────────────────────────────\nThông tin chi tiết về giá chưa được cấu hình trong CLI này.",
|
||||
"CmdTokensCacheBoth": "{hit} hit / {miss} miss",
|
||||
"CmdTokensCacheHitOnly": "{hit} hit / không báo cáo miss",
|
||||
"CmdTokensCacheMissOnly": "không báo cáo hit / {miss} miss",
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
"CmdCacheHeader": "缓存遥测 —— 最近 {count} / {total} 轮(模型:{model})\n",
|
||||
"CmdCacheNoData": "缓存历史:尚未记录任何轮次。\n\nDeepSeek 在受支持的模型(V4 系列)每个 API 轮次都会返回 `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`。请先运行一个轮次再试 /cache。",
|
||||
"CmdCacheTotals": "Σ 输入:{sum_in} Σ 命中:{sum_hit} Σ 未命中:{sum_miss} 平均命中率:{avg}\n",
|
||||
"CmdCostReport": "会话费用:\n─────────────────────────────\n预估累计消耗:{cost}\n\n费用为估算值;如有提供方用量遥测会优先使用。\n\nDeepSeek API 计费:\n─────────────────────────────\n此 CLI 中未配置详细计费规则。",
|
||||
"CmdCostReport": "会话费用:\n─────────────────────────────\n预估累计消耗:{cost}\n\n费用为估算值;如有提供方用量遥测会优先使用。\n\n提供商 API 计费:\n─────────────────────────────\n此 CLI 中未配置详细计费规则。",
|
||||
"CmdTokensCacheBoth": "命中 {hit} / 未命中 {miss}",
|
||||
"CmdTokensCacheHitOnly": "命中 {hit} / 未命中未上报",
|
||||
"CmdTokensCacheMissOnly": "命中未上报 / 未命中 {miss}",
|
||||
|
||||
@@ -121,6 +121,8 @@ fn test_cost_shows_spending_info() {
|
||||
assert!(msg.contains("Approx total spent:"));
|
||||
assert!(msg.contains("approximate"));
|
||||
assert!(msg.contains("$0.1234"));
|
||||
assert!(msg.contains("Provider API Pricing"));
|
||||
assert!(!msg.contains("DeepSeek API Pricing"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//! keeps the composer / transcript event loops in `ui.rs` short and
|
||||
//! lets us add a new platform without touching the call sites.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
pub(super) fn has_control_like_modifier(modifiers: KeyModifiers) -> bool {
|
||||
@@ -58,8 +60,8 @@ pub(super) fn is_file_tree_toggle_shortcut(key: &KeyEvent) -> bool {
|
||||
ctrl_shift_e || cmd_shift_e
|
||||
}
|
||||
|
||||
pub(super) fn tool_details_shortcut_label() -> &'static str {
|
||||
"v"
|
||||
pub(super) fn tool_details_shortcut_label() -> Cow<'static, str> {
|
||||
crate::tui::shell_key_routing::tool_details_chord()
|
||||
}
|
||||
|
||||
pub(super) fn tool_details_shortcut_action_hint(noun: &str) -> String {
|
||||
@@ -268,4 +270,16 @@ mod tests {
|
||||
assert!(!is_select_all_shortcut(&alt_a));
|
||||
assert!(!is_select_all_shortcut(&plain_a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_details_hint_uses_the_routed_chord_not_plain_typing() {
|
||||
let label = tool_details_shortcut_label();
|
||||
|
||||
assert_eq!(label, crate::tui::shell_key_routing::tool_details_chord());
|
||||
assert_ne!(label, "v");
|
||||
assert_eq!(
|
||||
tool_details_shortcut_action_hint("full output"),
|
||||
format!("{label} opens full output")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ fn truncate_to_width(text: &str, width: usize) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
/// Compact working detail for the phase band: `×N · 12s`.
|
||||
/// Compact working detail for the phase band: `×N` for tools or `12s` while
|
||||
/// the model is thinking.
|
||||
/// Kept quieter than the classic footer's verbose tool-status line so the
|
||||
/// transcript owns the ledger and the strip only names the live pulse.
|
||||
fn working_detail(app: &App, activity: LiveActivity) -> Option<String> {
|
||||
@@ -100,7 +101,7 @@ fn working_detail(app: &App, activity: LiveActivity) -> Option<String> {
|
||||
.map(|started| started.elapsed().as_secs());
|
||||
match (running, secs) {
|
||||
(0, Some(secs)) if secs > 0 => Some(format!("{secs}s")),
|
||||
(n, Some(secs)) if n > 0 => Some(format!("×{n} · {secs}s")),
|
||||
(n, Some(_)) if n > 0 => Some(format!("×{n}")),
|
||||
(n, None) if n > 0 => Some(format!("×{n}")),
|
||||
_ => None,
|
||||
}
|
||||
@@ -385,7 +386,11 @@ mod tests {
|
||||
.map(|cell| cell.symbol())
|
||||
.collect::<String>();
|
||||
assert!(text.contains("using tool"), "{text}");
|
||||
assert!(text.contains("×1 · 12s"), "{text}");
|
||||
assert!(text.contains("×1"), "{text}");
|
||||
assert!(
|
||||
!text.contains("12s"),
|
||||
"tool elapsed time belongs to the live tool row: {text}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("run ×1"),
|
||||
"detail repeated the tool verb: {text}"
|
||||
@@ -445,6 +450,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_band_keeps_elapsed_time_when_model_is_thinking() {
|
||||
let mut app = test_app();
|
||||
app.turn_started_at = Some(Instant::now() - Duration::from_secs(12));
|
||||
|
||||
assert_eq!(
|
||||
working_detail(&app, LiveActivity::from_app(&app)).as_deref(),
|
||||
Some("12s")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_band_keeps_unresolved_warning_visible() {
|
||||
let mut app = test_app();
|
||||
|
||||
@@ -6196,8 +6196,8 @@ mod tests {
|
||||
"repeated polling should collapse into one row: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
text.iter()
|
||||
.any(|line| line.contains(crate::tui::key_shortcuts::tool_details_shortcut_label())),
|
||||
text.iter().any(|line| line
|
||||
.contains(crate::tui::key_shortcuts::tool_details_shortcut_label().as_ref())),
|
||||
"collapsed CI row should point to details: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
|
||||
@@ -4558,7 +4558,7 @@ fn active_tool_status_label_summarizes_live_tool_group() {
|
||||
assert!(label.contains("cargo test"));
|
||||
assert!(label.contains("1 active"));
|
||||
assert!(label.contains("1 done"));
|
||||
assert!(label.contains(crate::tui::key_shortcuts::tool_details_shortcut_label()));
|
||||
assert!(label.contains(crate::tui::key_shortcuts::tool_details_shortcut_label().as_ref()));
|
||||
assert!(label.contains("opens details"));
|
||||
}
|
||||
|
||||
|
||||
@@ -335,12 +335,12 @@ impl ShellPhase {
|
||||
}
|
||||
return Self::Working;
|
||||
}
|
||||
if matches!(app.runtime_turn_status.as_deref(), Some("completed")) {
|
||||
return Self::Done;
|
||||
}
|
||||
if !app.input.is_empty() {
|
||||
return Self::Typing;
|
||||
}
|
||||
if matches!(app.runtime_turn_status.as_deref(), Some("completed")) {
|
||||
return Self::Done;
|
||||
}
|
||||
Self::Idle
|
||||
}
|
||||
|
||||
@@ -1934,4 +1934,15 @@ mod tests {
|
||||
assert_eq!(marker, "✓");
|
||||
assert_eq!(label, "done");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_phase_beats_stale_completion_status() {
|
||||
let mut app = test_app();
|
||||
app.runtime_turn_status = Some("completed".to_string());
|
||||
|
||||
assert_eq!(ShellPhase::from_app(&app), ShellPhase::Done);
|
||||
|
||||
app.input = "next task".to_string();
|
||||
assert_eq!(ShellPhase::from_app(&app), ShellPhase::Typing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2428,7 +2428,7 @@ fn experimental_feature_value(effective: bool, default_enabled: bool, configured
|
||||
fn config_label_for_key(key: &str) -> String {
|
||||
let static_label = match key {
|
||||
"provider" => "Active provider",
|
||||
"base_url" => "DeepSeek API URL",
|
||||
"base_url" => "Provider API URL (DeepSeek route)",
|
||||
"provider_url" => "Provider API URL",
|
||||
"model" => "Active provider model",
|
||||
"fast_model" => "Fast model (derived)",
|
||||
@@ -4967,6 +4967,10 @@ max_spawn_depth = 2
|
||||
.iter()
|
||||
.find(|row| row.key == "base_url")
|
||||
.expect("base_url row missing");
|
||||
assert_eq!(
|
||||
config_label_for_key(&row.key),
|
||||
"Provider API URL (DeepSeek route)"
|
||||
);
|
||||
assert_eq!(row.value, "https://ui-config-view.local/v1");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user