Skip to content
Closed
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
8 changes: 5 additions & 3 deletions crates/tui/src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::config_persistence::{
persist_tui_integer_key,
};
use crate::config_ui::{ConfigUiMode, parse_mode};
use crate::localization::resolve_locale;
use crate::localization::{MessageId, resolve_locale, tr};
use crate::settings::Settings;
use crate::tui::app::{
App, AppAction, AppMode, OnboardingState, ReasoningEffort, SidebarFocus, VimMode,
Expand Down Expand Up @@ -867,14 +867,15 @@ pub fn switch_mode(app: &mut App, mode: AppMode) -> String {
}

fn switch_mode_with_status(app: &mut App, mode: AppMode) -> (String, bool) {
let locale = app.ui_locale;
if app.set_mode(mode) {
(
format!("Switched to {} mode.", mode_display_name(mode)),
tr(locale, MessageId::AppModeSwitched).replace("{mode}", mode_display_name(mode)),
true,
)
} else {
(
format!("Already in {} mode.", mode_display_name(mode)),
tr(locale, MessageId::AppModeAlreadyIn).replace("{mode}", mode_display_name(mode)),
false,
)
}
Expand Down Expand Up @@ -1251,6 +1252,7 @@ mod tests {
app.auto_model = false;
app.api_provider = crate::config::ApiProvider::Deepseek;
app.model_ids_passthrough = false;
app.ui_locale = crate::localization::Locale::En;
app
}

Expand Down
106 changes: 106 additions & 0 deletions crates/tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,16 @@ pub enum MessageId {
CtxInspChangesByTurn,
CtxInspStablePrefixOnly,
CtxInspCacheTip,
// Onboarding screens — welcome screen.
OnboardWelcomeVersion,
OnboardWelcomeDesc,
OnboardWelcomeDesc2,
OnboardWelcomeDesc3,
OnboardWelcomeEnter,
OnboardWelcomeExit,
// App mode status messages.
AppModeSwitched,
AppModeAlreadyIn,
}

#[allow(dead_code)]
Expand Down Expand Up @@ -874,6 +884,14 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CtxInspChangesByTurn,
MessageId::CtxInspStablePrefixOnly,
MessageId::CtxInspCacheTip,
MessageId::OnboardWelcomeVersion,
MessageId::OnboardWelcomeDesc,
MessageId::OnboardWelcomeDesc2,
MessageId::OnboardWelcomeDesc3,
MessageId::OnboardWelcomeEnter,
MessageId::OnboardWelcomeExit,
MessageId::AppModeSwitched,
MessageId::AppModeAlreadyIn,
];

pub fn tr(locale: Locale, id: MessageId) -> &'static str {
Expand Down Expand Up @@ -1509,6 +1527,18 @@ fn english(id: MessageId) -> &'static str {
"Tip: Stable prefix blocks are DeepSeek V4 prefix-cache eligible. \
Volatile working-set changes break the cache only for the tail."
}
MessageId::OnboardWelcomeVersion => "Version {version}",
MessageId::OnboardWelcomeDesc => "A focused terminal workspace for longer model sessions.",
MessageId::OnboardWelcomeDesc2 => {
"You'll add an API key, review trust for this directory, and then land in the chat."
}
MessageId::OnboardWelcomeDesc3 => {
"The main composer is multi-line, so you can write full prompts instead of squeezing everything into one line."
}
MessageId::OnboardWelcomeEnter => "Press Enter to continue.",
MessageId::OnboardWelcomeExit => "Ctrl+C exits at any point.",
MessageId::AppModeSwitched => "Switched to {mode} mode",
MessageId::AppModeAlreadyIn => "Already in {mode} mode",
}
}

Expand Down Expand Up @@ -2009,6 +2039,20 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Gợi ý: Các khối ổn định đủ điều kiện cho bộ nhớ đệm tiền tố DeepSeek V4. Thay đổi vùng làm việc chỉ phá vỡ bộ nhớ đệm ở phần cuối."
}
MessageId::OnboardWelcomeVersion => "Phiên bản {version}",
MessageId::OnboardWelcomeDesc => {
"Một không gian làm việc đầu cuối tập trung cho các phiên làm việc với mô hình dài hơn."
}
MessageId::OnboardWelcomeDesc2 => {
"Bạn sẽ thêm khóa API, xem xét quyền truy cập cho thư mục này, và sau đó vào cuộc trò chuyện."
}
MessageId::OnboardWelcomeDesc3 => {
"Trình soạn thảo chính hỗ trợ nhiều dòng, vì vậy bạn có thể viết đầy đủ nội dung thay vì nhồi nhét mọi thứ vào một dòng."
}
MessageId::OnboardWelcomeEnter => "Nhấn Enter để tiếp tục.",
MessageId::OnboardWelcomeExit => "Ctrl+C thoát bất kỳ lúc nào.",
MessageId::AppModeSwitched => "Đã chuyển sang chế độ {mode}",
MessageId::AppModeAlreadyIn => "Đã ở chế độ {mode} rồi",
})
}

Expand Down Expand Up @@ -2073,6 +2117,16 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"提示:穩定前綴區塊符合 DeepSeek V4 前綴快取條件。易變工作集的更改僅會破壞快取尾部。"
}
MessageId::OnboardWelcomeVersion => "版本 {version}",
MessageId::OnboardWelcomeDesc => "專注於更長模型會話的終端工作區。",
MessageId::OnboardWelcomeDesc2 => "您將添加 API 金鑰、審閱此目錄的信任設定,然後進入對話。",
MessageId::OnboardWelcomeDesc3 => {
"主編輯器支援多行輸入,因此您可以撰寫完整的提示,而不是將所有內容擠在一行中。"
}
MessageId::OnboardWelcomeEnter => "按 Enter 繼續。",
MessageId::OnboardWelcomeExit => "Ctrl+C 隨時退出。",
MessageId::AppModeSwitched => "已切換至 {mode} 模式",
MessageId::AppModeAlreadyIn => "已在 {mode} 模式",
other => chinese_simplified(other)?,
})
}
Expand Down Expand Up @@ -2536,6 +2590,20 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"ヒント:安定プレフィックスブロックはDeepSeek V4プレフィックスキャッシュの対象です。揮発性ワーキングセットの変更は末尾のキャッシュのみを破壊します。"
}
MessageId::OnboardWelcomeVersion => "バージョン {version}",
MessageId::OnboardWelcomeDesc => {
"長時間のモデルセッションに最適化された端末ワークスペース。"
}
MessageId::OnboardWelcomeDesc2 => {
"APIキーを追加し、このディレクトリの信頼設定を確認してから、チャットを開始します。"
}
MessageId::OnboardWelcomeDesc3 => {
"メインコンポーザーはマルチライン対応なので、すべてを1行に詰め込む代わりに完全なプロンプトを記述できます。"
}
MessageId::OnboardWelcomeEnter => "Enter を押して続行。",
MessageId::OnboardWelcomeExit => "Ctrl+C でいつでも終了。",
MessageId::AppModeSwitched => "{mode} モードに切り替えました",
MessageId::AppModeAlreadyIn => "既に {mode} モードです",
})
}

Expand Down Expand Up @@ -2940,6 +3008,16 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"提示:稳定前缀区块符合 DeepSeek V4 前缀缓存条件。易变工作集的更改仅会破坏缓存尾部。"
}
MessageId::OnboardWelcomeVersion => "版本 {version}",
MessageId::OnboardWelcomeDesc => "专注于更长模型会话的终端工作区。",
MessageId::OnboardWelcomeDesc2 => "您将添加 API 密钥、审阅此目录的信任设置,然后进入对话。",
MessageId::OnboardWelcomeDesc3 => {
"主编辑器支持多行输入,因此您可以编写完整的提示,而不是将所有内容挤在一行中。"
}
MessageId::OnboardWelcomeEnter => "按 Enter 继续。",
MessageId::OnboardWelcomeExit => "Ctrl+C 随时退出。",
MessageId::AppModeSwitched => "已切换至 {mode} 模式",
MessageId::AppModeAlreadyIn => "已在 {mode} 模式",
})
}

Expand Down Expand Up @@ -3426,6 +3504,20 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Dica: Blocos de prefixo estável são elegíveis para cache de prefixo DeepSeek V4. Alterações no conjunto de trabalho volátil quebram o cache apenas no final."
}
MessageId::OnboardWelcomeVersion => "Versão {version}",
MessageId::OnboardWelcomeDesc => {
"Um espaço de trabalho terminal focado para sessões de modelo mais longas."
}
MessageId::OnboardWelcomeDesc2 => {
"Você adicionará uma chave de API, revisará a confiança para este diretório e então entrará no chat."
}
MessageId::OnboardWelcomeDesc3 => {
"O compositor principal é multi-linha, permitindo prompts completos em vez de comprimir tudo em uma única linha."
}
MessageId::OnboardWelcomeEnter => "Pressione Enter para continuar.",
MessageId::OnboardWelcomeExit => "Ctrl+C sai a qualquer momento.",
MessageId::AppModeSwitched => "Alternado para o modo {mode}",
MessageId::AppModeAlreadyIn => "Já está no modo {mode}",
})
}

Expand Down Expand Up @@ -3922,6 +4014,20 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
MessageId::CtxInspCacheTip => {
"Consejo: Los bloques de prefijo estable son elegibles para caché de prefijo DeepSeek V4. Los cambios en el conjunto de trabajo volátil solo rompen la caché al final."
}
MessageId::OnboardWelcomeVersion => "Versión {version}",
MessageId::OnboardWelcomeDesc => {
"Un espacio de trabajo terminal enfocado para sesiones de modelo más largas."
}
MessageId::OnboardWelcomeDesc2 => {
"Agregarás una clave de API, revisarás la confianza para este directorio y luego ingresarás al chat."
}
MessageId::OnboardWelcomeDesc3 => {
"El compositor principal es multilínea, permitiendo prompts completos en lugar de comprimir todo en una línea."
}
MessageId::OnboardWelcomeEnter => "Presiona Enter para continuar.",
MessageId::OnboardWelcomeExit => "Ctrl+C sale en cualquier momento.",
MessageId::AppModeSwitched => "Cambiado al modo {mode}",
MessageId::AppModeAlreadyIn => "Ya está en modo {mode}",
})
}

Expand Down
53 changes: 43 additions & 10 deletions crates/tui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,18 @@ pub enum StatusToastLevel {
Error,
}

#[derive(Debug, Clone)]
pub enum StatusToastKind {
ModeSwitch,
}

#[derive(Debug, Clone)]
pub struct StatusToast {
pub text: String,
pub level: StatusToastLevel,
pub created_at: Instant,
pub ttl_ms: Option<u64>,
pub is_mode_switch: bool,
}

impl StatusToast {
Expand All @@ -392,6 +398,7 @@ impl StatusToast {
level,
created_at: Instant::now(),
ttl_ms,
is_mode_switch: false,
}
}

Expand Down Expand Up @@ -1282,6 +1289,10 @@ pub struct App {
pub sticky_status: Option<StatusToast>,
/// Last status text already promoted from `status_message` into toast state.
pub last_status_message_seen: Option<String>,
/// When the next status message is promoted, it will be tagged with this
/// variant so downstream checks (mode-switch dedup) work regardless of
/// locale.
pub last_status_toast_kind: Option<StatusToastKind>,
pub model: String,
/// Persisted model selections by provider name. Loaded from settings so
/// `/model` and the picker can surface saved provider-specific choices.
Expand Down Expand Up @@ -2106,6 +2117,7 @@ impl App {
status_toasts: VecDeque::new(),
sticky_status: None,
last_status_message_seen: None,
last_status_toast_kind: None,
model,
provider_models,
auto_model,
Expand Down Expand Up @@ -2381,7 +2393,9 @@ impl App {
let entering_yolo = mode == AppMode::Yolo && previous_mode != AppMode::Yolo;
let leaving_yolo = previous_mode == AppMode::Yolo && mode != AppMode::Yolo;
self.mode = mode;
self.status_message = Some(format!("Switched to {} mode", mode.label()));
self.status_message =
Some(tr(self.ui_locale, MessageId::AppModeSwitched).replace("{mode}", mode.label()));
self.last_status_toast_kind = Some(StatusToastKind::ModeSwitch);

if entering_yolo {
self.yolo_restore = Some(YoloRestoreState {
Expand Down Expand Up @@ -3179,7 +3193,23 @@ impl App {
level: StatusToastLevel,
ttl_ms: Option<u64>,
) {
let toast = StatusToast::new(text, level, ttl_ms);
self.push_status_toast_with_flags(text, level, ttl_ms, false);
}

fn push_status_toast_with_flags(
&mut self,
text: impl Into<String>,
level: StatusToastLevel,
ttl_ms: Option<u64>,
is_mode_switch: bool,
) {
let toast = StatusToast {
text: text.into(),
level,
created_at: Instant::now(),
ttl_ms,
is_mode_switch,
};
self.status_toasts.push_back(toast);
while self.status_toasts.len() > 24 {
self.status_toasts.pop_front();
Expand Down Expand Up @@ -3317,10 +3347,6 @@ impl App {
(StatusToastLevel::Info, Some(4_000), false)
}

fn is_mode_switch_status_message(message: &str) -> bool {
message.starts_with("Switched to ") && message.ends_with(" mode")
}

pub fn sync_status_message_to_toasts(&mut self) {
let current = self.status_message.clone();
if self.last_status_message_seen == current {
Expand All @@ -3335,6 +3361,12 @@ impl App {
return;
}

let is_mode_switch = matches!(
self.last_status_toast_kind,
Some(StatusToastKind::ModeSwitch)
);
self.last_status_toast_kind = None;

let (level, ttl_ms, sticky) = Self::classify_status_text(&message);
if sticky {
self.set_sticky_status(message, level, ttl_ms);
Expand All @@ -3347,11 +3379,10 @@ impl App {
{
self.clear_sticky_status();
}
if Self::is_mode_switch_status_message(&message) {
self.status_toasts
.retain(|toast| !Self::is_mode_switch_status_message(&toast.text));
if is_mode_switch {
self.status_toasts.retain(|toast| !toast.is_mode_switch);
}
self.push_status_toast(message, level, ttl_ms);
self.push_status_toast_with_flags(message, level, ttl_ms, is_mode_switch);
}
}

Expand Down Expand Up @@ -6135,6 +6166,7 @@ mod tests {
#[test]
fn test_mode_switch_toasts_replace_previous_mode_switch_toast() {
let mut app = App::new(test_options(false), &Config::default());
app.ui_locale = Locale::En;
let first_mode = match app.mode {
AppMode::Plan => AppMode::Agent,
AppMode::Agent => AppMode::Yolo,
Expand Down Expand Up @@ -6179,6 +6211,7 @@ mod tests {
#[test]
fn test_mode_switch_toasts_do_not_disrupt_non_mode_toasts() {
let mut app = App::new(test_options(false), &Config::default());
app.ui_locale = Locale::En;
app.status_message = Some("Task queued".to_string());
app.sync_status_message_to_toasts();

Expand Down
2 changes: 1 addition & 1 deletion crates/tui/src/tui/onboarding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
};

let lines = match app.onboarding {
OnboardingState::Welcome => welcome::lines(),
OnboardingState::Welcome => welcome::lines(app.ui_locale),
OnboardingState::Language => language::lines(app),
OnboardingState::ApiKey => api_key::lines(app),
OnboardingState::TrustDirectory => trust_directory::lines(app),
Expand Down
Loading
Loading