From 0a8163dec002cfbdad63ab4899583945dff0e543 Mon Sep 17 00:00:00 2001 From: liuzixiang Date: Mon, 19 Oct 2026 12:12:25 +0800 Subject: [PATCH 1/2] fix(sessions): recognize the German, French and Spanish placeholder titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLACEHOLDER_TITLES had no entries for the German, French, and Spanish chat.untitledTask labels even though those locales ship their own text (Neue Aufgabe / Nouvelle tâche / Nueva tarea) — the comment claimed they fall back to the English label, and they do not. The host therefore recorded those sessions as title_source='manual' and the first prompt never re-titled them: automatic and plugin-provided titles stayed dead in all three locales (#1755). #1725 fixed ASCII case-insensitivity; the missing locales remained. The three lowercased labels join PLACEHOLDER_TITLES and the renderer's LEGACY_DEFAULT_TITLES, with a behavioral test over the real catalogs on both sides. Also corrects the AppSettings doc comment that still said the sidebar running affordances default off after #1749 defaulted them on. --- apps/desktop/src/lib/session-title-utils.ts | 3 ++ .../session-title-prompt-fallback.test.mjs | 35 +++++++++++++++++++ crates/host-core/src/sessions.rs | 24 ++++++++++--- packages/shared/src/types/settings.ts | 9 ++--- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/lib/session-title-utils.ts b/apps/desktop/src/lib/session-title-utils.ts index ce856512f7..e6275efc6d 100644 --- a/apps/desktop/src/lib/session-title-utils.ts +++ b/apps/desktop/src/lib/session-title-utils.ts @@ -18,6 +18,9 @@ const LEGACY_DEFAULT_TITLES = new Set([ "nova conversa", "yeni görev", "yeni sohbet", + "neue aufgabe", + "nouvelle tâche", + "nueva tarea", ]); /** Sidebar/topbar title budget for the local first-prompt fallback. */ const PROMPT_FALLBACK_TITLE_LENGTH = 48; diff --git a/apps/desktop/test/session-title-prompt-fallback.test.mjs b/apps/desktop/test/session-title-prompt-fallback.test.mjs index 9ccf46094f..22194a4282 100644 --- a/apps/desktop/test/session-title-prompt-fallback.test.mjs +++ b/apps/desktop/test/session-title-prompt-fallback.test.mjs @@ -96,3 +96,38 @@ test("automatic AI titles stay plugin-owned", () => { assert.doesNotMatch(store, /triggerAutoTitleSummarization/); assert.doesNotMatch(main, /IPC\.invoke\.sessionSummarizeTitle/); }); + +test("the German, French and Spanish placeholders are recognized as default titles (#1755)", async (t) => { + // The renderer lowercases chat.untitledTask before persisting; the host + // list and this legacy set must recognize those forms or auto-titling + // never runs in those locales. + const locales = [ + ["de", "Neue Aufgabe", "neue aufgabe"], + ["fr", "Nouvelle tâche", "nouvelle tâche"], + ["es", "Nueva tarea", "nueva tarea"], + ]; + for (const [locale, label, lowercased] of locales) { + assert.equal( + catalogs[locale].chat.untitledTask, + label, + `${locale} catalog must still own its placeholder label`, + ); + assert.equal( + isDefaultSessionTitle(label), + true, + `${locale}: the placeholder is recognized as default`, + ); + assert.equal( + isDefaultSessionTitle(lowercased), + true, + `${locale}: the persisted lowercased form is recognized too`, + ); + } + + // Host side stays in sync: the three labels are in PLACEHOLDER_TITLES. + assert.match( + host, + /"neue aufgabe",\s*\n\s*"nouvelle tâche",\s*\n\s*"nueva tarea",/, + "PLACEHOLDER_TITLES must carry the German, French and Spanish labels", + ); +}); diff --git a/crates/host-core/src/sessions.rs b/crates/host-core/src/sessions.rs index df89a3dcc5..1ad90e967d 100644 --- a/crates/host-core/src/sessions.rs +++ b/crates/host-core/src/sessions.rs @@ -342,11 +342,13 @@ pub struct SearchHit { /// The renderer creates a session with its localized `chat.untitledTask` label /// and also recognizes `nav.newChat`, so host-core has to recognize the same /// values: it owns title eligibility and cannot read the renderer catalog when -/// it decides whether a title may still be replaced. German, Spanish, and -/// French fall back to the English label. Keep this in sync with the migration -/// list in `db/session_title_source_migration.rs` and `LEGACY_DEFAULT_TITLES` -/// in the renderer. -const PLACEHOLDER_TITLES: [&str; 13] = [ +/// it decides whether a title may still be replaced. Entries are stored in +/// the form the renderer writes: lowercased, because every localized +/// `chat.untitledTask` has its own label — the German, Spanish, and French +/// ones do not fall back to the English text (#1755). Keep this in sync with +/// the migration list in `db/session_title_source_migration.rs` and +/// `LEGACY_DEFAULT_TITLES` in the renderer. +const PLACEHOLDER_TITLES: [&str; 16] = [ "", "New task", "New chat", @@ -360,6 +362,9 @@ const PLACEHOLDER_TITLES: [&str; 13] = [ "Nova conversa", "Yeni görev", "Yeni sohbet", + "neue aufgabe", + "nouvelle tâche", + "nueva tarea", ]; fn is_default_title(title: &str) -> bool { @@ -4269,6 +4274,15 @@ pub fn get_token_usage_history( #[cfg(test)] mod tests { use super::*; + + #[test] + fn default_title_matches_the_german_french_and_spanish_placeholders() { + // The renderer lowercases `chat.untitledTask` before persisting (#1755). + assert!(is_default_title("neue aufgabe")); + assert!(is_default_title("Nouvelle tâche")); + assert!(is_default_title("nueva tarea")); + assert!(!is_default_title("real session name")); + } use crate::workspace::simple_canonicalize; fn test_db() -> Database { diff --git a/packages/shared/src/types/settings.ts b/packages/shared/src/types/settings.ts index 2bf997b9e3..26d6d96cb4 100644 --- a/packages/shared/src/types/settings.ts +++ b/packages/shared/src/types/settings.ts @@ -172,13 +172,14 @@ export type AppSettings = { preventScreenSleep?: boolean; /** * Sidebar project "running" affordances. The master switch gates both - * sub-switches; absent means disabled, so a fresh install shows a clean - * sidebar until the user opts in. + * sub-switches; absent means enabled — the renderer normalizes + * `!== false` to on, so a fresh install shows the running badge and rail + * until the user turns them off (#1749). */ sidebarRunningIndicators?: boolean; - /** Count badge beside a project name. Only honored while the master is on. */ + /** Count badge beside a project name. On unless explicitly disabled. */ sidebarRunningBadge?: boolean; - /** Activity rail down the project group. Only honored while the master is on. */ + /** Activity rail down the project group. On unless explicitly disabled. */ sidebarRunningRail?: boolean; /** * Last-reply date shown at the right edge of session rows. Absent means From b2f29a4f1fa8448b7d8bee36eb1d5af7e1a50fae Mon Sep 17 00:00:00 2001 From: liuzixiang Date: Mon, 19 Oct 2026 18:49:36 +0800 Subject: [PATCH 2/2] fix(sessions): close the title sync gaps from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on #1757: the v22→v23 migration list gains the six German/French/Spanish labels it was missing (future databases migrating from v22 classify those rows default instead of manual), PLACEHOLDER_TITLES now also carries the nav.newChat labels for the locales that had no entry yet (neuer chat / nuevo chat / nouvelle discussion — sessions created from that label were treated as manually named in every locale), and the whole list is stored lowercase with is_default_title comparing through to_lowercase, so a future entry with a non-ASCII uppercase letter cannot slip past ASCII-case-insensitive matching. --- apps/desktop/src/lib/session-title-utils.ts | 3 ++ .../session-title-prompt-fallback.test.mjs | 42 ++++++++++++------- .../src/db/session_title_source_migration.rs | 4 +- crates/host-core/src/sessions.rs | 41 +++++++++++------- 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/lib/session-title-utils.ts b/apps/desktop/src/lib/session-title-utils.ts index e6275efc6d..26f975adeb 100644 --- a/apps/desktop/src/lib/session-title-utils.ts +++ b/apps/desktop/src/lib/session-title-utils.ts @@ -21,6 +21,9 @@ const LEGACY_DEFAULT_TITLES = new Set([ "neue aufgabe", "nouvelle tâche", "nueva tarea", + "neuer chat", + "nuevo chat", + "nouvelle discussion", ]); /** Sidebar/topbar title budget for the local first-prompt fallback. */ const PROMPT_FALLBACK_TITLE_LENGTH = 48; diff --git a/apps/desktop/test/session-title-prompt-fallback.test.mjs b/apps/desktop/test/session-title-prompt-fallback.test.mjs index 22194a4282..a4d7ee5349 100644 --- a/apps/desktop/test/session-title-prompt-fallback.test.mjs +++ b/apps/desktop/test/session-title-prompt-fallback.test.mjs @@ -100,34 +100,48 @@ test("automatic AI titles stay plugin-owned", () => { test("the German, French and Spanish placeholders are recognized as default titles (#1755)", async (t) => { // The renderer lowercases chat.untitledTask before persisting; the host // list and this legacy set must recognize those forms or auto-titling - // never runs in those locales. + // never runs in those locales. nav.newChat resolves through the same + // two-sided comparison. const locales = [ - ["de", "Neue Aufgabe", "neue aufgabe"], - ["fr", "Nouvelle tâche", "nouvelle tâche"], - ["es", "Nueva tarea", "nueva tarea"], + ["de", "Neue Aufgabe", "Neuer Chat"], + ["fr", "Nouvelle tâche", "Nouvelle discussion"], + ["es", "Nueva tarea", "Nuevo chat"], ]; - for (const [locale, label, lowercased] of locales) { + for (const [locale, untitled, newChat] of locales) { assert.equal( catalogs[locale].chat.untitledTask, - label, - `${locale} catalog must still own its placeholder label`, + untitled, + `${locale} catalog must still own its untitledTask label`, ); assert.equal( - isDefaultSessionTitle(label), + catalogs[locale].nav.newChat, + newChat, + `${locale} catalog must still own its newChat label`, + ); + assert.equal(isDefaultSessionTitle(untitled), true, `${locale} untitledTask`); + assert.equal( + isDefaultSessionTitle(untitled.toLowerCase()), true, - `${locale}: the placeholder is recognized as default`, + `${locale} untitledTask lowercased`, ); + assert.equal(isDefaultSessionTitle(newChat), true, `${locale} newChat`); assert.equal( - isDefaultSessionTitle(lowercased), + isDefaultSessionTitle(newChat.toLowerCase()), true, - `${locale}: the persisted lowercased form is recognized too`, + `${locale} newChat lowercased`, ); } - // Host side stays in sync: the three labels are in PLACEHOLDER_TITLES. + // Host side stays in sync: both label families, stored lowercase, compared + // through to_lowercase so a non-ASCII uppercase letter cannot slip past. + assert.match( + host, + /"neue aufgabe",\s*\n\s*"nouvelle tâche",\s*\n\s*"nueva tarea",\s*\n\s*"neuer chat",\s*\n\s*"nuevo chat",\s*\n\s*"nouvelle discussion",/, + "PLACEHOLDER_TITLES must carry the German, French and Spanish labels for both keys", + ); assert.match( host, - /"neue aufgabe",\s*\n\s*"nouvelle tâche",\s*\n\s*"nueva tarea",/, - "PLACEHOLDER_TITLES must carry the German, French and Spanish labels", + /to_lowercase\(\)/, + "is_default_title compares through to_lowercase", ); }); diff --git a/crates/host-core/src/db/session_title_source_migration.rs b/crates/host-core/src/db/session_title_source_migration.rs index 8c853a6a8d..0cc485c8e7 100644 --- a/crates/host-core/src/db/session_title_source_migration.rs +++ b/crates/host-core/src/db/session_title_source_migration.rs @@ -23,7 +23,9 @@ fn migrate_v22_to_v23_tx(tx: &Transaction<'_>) -> Result<()> { WHEN title IN ('', 'New task', 'New chat', '新建任务', '新对话', '新建任務', '新對話', '새 작업', '새 채팅', 'Tarefa sem título', 'Nova conversa', - 'Yeni görev', 'Yeni sohbet') THEN 'default' + 'Yeni görev', 'Yeni sohbet', 'Neue Aufgabe', + 'Nouvelle tâche', 'Nueva tarea', 'Neuer Chat', + 'Nuevo chat', 'Nouvelle discussion') THEN 'default' ELSE 'manual' END", [], diff --git a/crates/host-core/src/sessions.rs b/crates/host-core/src/sessions.rs index 1ad90e967d..fd7a3650d1 100644 --- a/crates/host-core/src/sessions.rs +++ b/crates/host-core/src/sessions.rs @@ -342,34 +342,39 @@ pub struct SearchHit { /// The renderer creates a session with its localized `chat.untitledTask` label /// and also recognizes `nav.newChat`, so host-core has to recognize the same /// values: it owns title eligibility and cannot read the renderer catalog when -/// it decides whether a title may still be replaced. Entries are stored in -/// the form the renderer writes: lowercased, because every localized -/// `chat.untitledTask` has its own label — the German, Spanish, and French -/// ones do not fall back to the English text (#1755). Keep this in sync with -/// the migration list in `db/session_title_source_migration.rs` and -/// `LEGACY_DEFAULT_TITLES` in the renderer. -const PLACEHOLDER_TITLES: [&str; 16] = [ +/// it decides whether a title may still be replaced. Every entry is stored +/// lowercased and compared through `to_lowercase`, because both localized +/// keys the renderer writes — `chat.untitledTask` and `nav.newChat` — have +/// their own label per locale, and a non-ASCII uppercase letter (a future +/// "Ñö..." entry) would slip past ASCII-case-insensitive matching (#1755). +/// Keep this in sync with the migration list in +/// `db/session_title_source_migration.rs` and `LEGACY_DEFAULT_TITLES` in the +/// renderer. +const PLACEHOLDER_TITLES: [&str; 19] = [ "", - "New task", - "New chat", + "new task", + "new chat", "新建任务", "新对话", "新建任務", "新對話", "새 작업", "새 채팅", - "Tarefa sem título", - "Nova conversa", - "Yeni görev", - "Yeni sohbet", + "tarefa sem título", + "nova conversa", + "yeni görev", + "yeni sohbet", "neue aufgabe", "nouvelle tâche", "nueva tarea", + "neuer chat", + "nuevo chat", + "nouvelle discussion", ]; fn is_default_title(title: &str) -> bool { - let t = title.trim(); - PLACEHOLDER_TITLES.iter().any(|p| p.eq_ignore_ascii_case(t)) + let t = title.trim().to_lowercase(); + PLACEHOLDER_TITLES.iter().any(|p| *p == t) } // ---- UiMessage ⇄ transcript record mapping ----------------------------------- @@ -4277,10 +4282,14 @@ mod tests { #[test] fn default_title_matches_the_german_french_and_spanish_placeholders() { - // The renderer lowercases `chat.untitledTask` before persisting (#1755). + // The renderer lowercases `chat.untitledTask` before persisting (#1755); + // the nav.newChat labels resolve through the same to_lowercase path. assert!(is_default_title("neue aufgabe")); assert!(is_default_title("Nouvelle tâche")); assert!(is_default_title("nueva tarea")); + assert!(is_default_title("Neuer Chat")); + assert!(is_default_title("nuevo chat")); + assert!(is_default_title("Nouvelle discussion")); assert!(!is_default_title("real session name")); } use crate::workspace::simple_canonicalize;