+
{localize('com_ui_delete_confirm')} {title} ?
diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts
index 4a5337fe39..9fd3b01885 100644
--- a/packages/api/src/utils/index.ts
+++ b/packages/api/src/utils/index.ts
@@ -10,6 +10,7 @@ export * from './key';
export * from './llm';
export * from './math';
export * from './openid';
+export * from './sanitizeTitle';
export * from './tempChatRetention';
export * from './text';
export { default as Tokenizer } from './tokenizer';
diff --git a/packages/api/src/utils/sanitizeTitle.spec.ts b/packages/api/src/utils/sanitizeTitle.spec.ts
new file mode 100644
index 0000000000..df03d1b7b4
--- /dev/null
+++ b/packages/api/src/utils/sanitizeTitle.spec.ts
@@ -0,0 +1,217 @@
+import { sanitizeTitle } from './sanitizeTitle';
+
+describe('sanitizeTitle', () => {
+ describe('Happy Path', () => {
+ it('should remove a single think block and return the clean title', () => {
+ const input = 'This is reasoning about the topic User Hi Greeting';
+ expect(sanitizeTitle(input)).toBe('User Hi Greeting');
+ });
+
+ it('should handle thinking block at the start', () => {
+ const input = 'reasoning here Clean Title Text';
+ expect(sanitizeTitle(input)).toBe('Clean Title Text');
+ });
+
+ it('should handle thinking block at the end', () => {
+ const input = 'Clean Title Text reasoning here';
+ expect(sanitizeTitle(input)).toBe('Clean Title Text');
+ });
+
+ it('should handle title without any thinking blocks', () => {
+ const input = 'Just a Normal Title';
+ expect(sanitizeTitle(input)).toBe('Just a Normal Title');
+ });
+ });
+
+ describe('Multiple Blocks', () => {
+ it('should remove multiple think blocks', () => {
+ const input =
+ 'reason 1 Intro reason 2 Middle reason 3 Final';
+ expect(sanitizeTitle(input)).toBe('Intro Middle Final');
+ });
+
+ it('should handle consecutive think blocks', () => {
+ const input = 'r1r2Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+ });
+
+ describe('Case Insensitivity', () => {
+ it('should handle uppercase THINK tags', () => {
+ const input = 'reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should handle mixed case Think tags', () => {
+ const input = 'reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should handle mixed case closing tag', () => {
+ const input = 'reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+ });
+
+ describe('Attributes in Tags', () => {
+ it('should remove think tags with attributes', () => {
+ const input = 'reasoning here Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should handle multiple attributes', () => {
+ const input =
+ 'reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should handle single-quoted attributes', () => {
+ const input = "content Title";
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should handle unquoted attributes', () => {
+ const input = 'reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+ });
+
+ describe('Newlines and Multiline Content', () => {
+ it('should handle newlines within the think block', () => {
+ const input = `
+ This is a long reasoning
+ spanning multiple lines
+ with various thoughts
+ Clean Title`;
+ expect(sanitizeTitle(input)).toBe('Clean Title');
+ });
+
+ it('should handle newlines around tags', () => {
+ const input = `
+ reasoning
+ My Title
+ `;
+ expect(sanitizeTitle(input)).toBe('My Title');
+ });
+
+ it('should handle mixed whitespace', () => {
+ const input = '\n\t reasoning \t\n\n Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+ });
+
+ describe('Whitespace Normalization', () => {
+ it('should collapse multiple spaces', () => {
+ const input = 'x Multiple Spaces';
+ expect(sanitizeTitle(input)).toBe('Multiple Spaces');
+ });
+
+ it('should collapse mixed whitespace', () => {
+ const input = 'Start \n\t Middle x \n End';
+ expect(sanitizeTitle(input)).toBe('Start Middle End');
+ });
+
+ it('should trim leading whitespace', () => {
+ const input = ' reasoning Title';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+
+ it('should trim trailing whitespace', () => {
+ const input = 'Title reasoning \n ';
+ expect(sanitizeTitle(input)).toBe('Title');
+ });
+ });
+
+ describe('Empty and Fallback Cases', () => {
+ it('should return fallback for empty string', () => {
+ expect(sanitizeTitle('')).toBe('Untitled Conversation');
+ });
+
+ it('should return fallback when only whitespace remains', () => {
+ const input = 'thinking \n\t\r\n ';
+ expect(sanitizeTitle(input)).toBe('Untitled Conversation');
+ });
+
+ it('should return fallback when only think blocks exist', () => {
+ const input = 'just thinkingmore thinking';
+ expect(sanitizeTitle(input)).toBe('Untitled Conversation');
+ });
+
+ it('should return fallback for non-string whitespace', () => {
+ expect(sanitizeTitle(' ')).toBe('Untitled Conversation');
+ });
+ });
+
+ describe('Edge Cases and Real-World', () => {
+ it('should handle long reasoning blocks', () => {
+ const longReasoning =
+ 'This is a very long reasoning block ' + 'with lots of text. '.repeat(50);
+ const input = `${longReasoning} Final Title`;
+ expect(sanitizeTitle(input)).toBe('Final Title');
+ });
+
+ it('should handle nested-like patterns', () => {
+ const input = 'outer inner end Title';
+ const result = sanitizeTitle(input);
+ expect(result).toContain('Title');
+ });
+
+ it('should handle malformed tags missing closing', () => {
+ const input = 'unclosed reasoning. Title';
+ const result = sanitizeTitle(input);
+ expect(result).toContain('Title');
+ expect(result).toContain('');
+ });
+
+ it('should handle real-world LLM example', () => {
+ const input =
+ '\nThe user is asking for a greeting. I should provide a friendly response.\n User Hi Greeting';
+ expect(sanitizeTitle(input)).toBe('User Hi Greeting');
+ });
+
+ it('should handle real-world with attributes', () => {
+ const input =
+ '\nStep 1\nStep 2\n Project Status';
+ expect(sanitizeTitle(input)).toBe('Project Status');
+ });
+ });
+
+ describe('Idempotency', () => {
+ it('should be idempotent', () => {
+ const input = 'reasoning My Title';
+ const once = sanitizeTitle(input);
+ const twice = sanitizeTitle(once);
+ expect(once).toBe(twice);
+ expect(once).toBe('My Title');
+ });
+
+ it('should be idempotent with fallback', () => {
+ const input = 'only thinking';
+ const once = sanitizeTitle(input);
+ const twice = sanitizeTitle(once);
+ expect(once).toBe(twice);
+ expect(once).toBe('Untitled Conversation');
+ });
+ });
+
+ describe('Return Type Safety', () => {
+ it('should always return a string', () => {
+ expect(typeof sanitizeTitle('x Title')).toBe('string');
+ expect(typeof sanitizeTitle('No blocks')).toBe('string');
+ expect(typeof sanitizeTitle('')).toBe('string');
+ });
+
+ it('should never return empty', () => {
+ expect(sanitizeTitle('')).not.toBe('');
+ expect(sanitizeTitle(' ')).not.toBe('');
+ expect(sanitizeTitle('x')).not.toBe('');
+ });
+
+ it('should never return null or undefined', () => {
+ expect(sanitizeTitle('test')).not.toBeNull();
+ expect(sanitizeTitle('test')).not.toBeUndefined();
+ expect(sanitizeTitle('')).not.toBeNull();
+ expect(sanitizeTitle('')).not.toBeUndefined();
+ });
+ });
+});
diff --git a/packages/api/src/utils/sanitizeTitle.ts b/packages/api/src/utils/sanitizeTitle.ts
new file mode 100644
index 0000000000..ef03f071d0
--- /dev/null
+++ b/packages/api/src/utils/sanitizeTitle.ts
@@ -0,0 +1,30 @@
+/**
+ * Sanitizes LLM-generated chat titles by removing ... reasoning blocks.
+ *
+ * This function strips out all reasoning blocks (with optional attributes and newlines)
+ * and returns a clean title. If the result is empty, a fallback is returned.
+ *
+ * @param rawTitle - The raw LLM-generated title string, potentially containing blocks.
+ * @returns A sanitized title string, never empty (fallback used if needed).
+ */
+export function sanitizeTitle(rawTitle: string): string {
+ const DEFAULT_FALLBACK = 'Untitled Conversation';
+
+ // Step 1: Input Validation
+ if (!rawTitle || typeof rawTitle !== 'string') {
+ return DEFAULT_FALLBACK;
+ }
+
+ // Step 2: Build and apply the regex to remove all ... blocks
+ const thinkBlockRegex = /]*>[\s\S]*?<\/think>/gi;
+ const cleaned = rawTitle.replace(thinkBlockRegex, '');
+
+ // Step 3: Normalize whitespace (collapse multiple spaces/newlines to single space)
+ const normalized = cleaned.replace(/\s+/g, ' ');
+
+ // Step 4: Trim leading and trailing whitespace
+ const trimmed = normalized.trim();
+
+ // Step 5: Return trimmed result or fallback if empty
+ return trimmed.length > 0 ? trimmed : DEFAULT_FALLBACK;
+}
\ No newline at end of file