Skip to content

feat: fix AI session auto-renaming title extraction / 修复会话自动重命名标题提取#941

Open
WenhuaXia wants to merge 2 commits into
siteboon:mainfrom
WenhuaXia:feat/auto-rename-session-fix
Open

feat: fix AI session auto-renaming title extraction / 修复会话自动重命名标题提取#941
WenhuaXia wants to merge 2 commits into
siteboon:mainfrom
WenhuaXia:feat/auto-rename-session-fix

Conversation

@WenhuaXia

@WenhuaXia WenhuaXia commented Jun 30, 2026

Copy link
Copy Markdown

Problem / 问题

When the AI model quotes the user's message in its thinking block (e.g. - **User's message:** "..."), the session title extraction mistakenly used the user's own prompt as the session name.

AI 模型在 thinking 块中引用用户消息时,会话重命名逻辑将用户原话误当作标题。

Solution / 修复

  • isPromptMatch() strips markdown prefixes (markdown bold, wrapper quotes/backticks) before comparison
  • Added extractTitleFromThinking() for reasoning models (Qwen etc.) that produce thinking instead of text blocks
  • Added truncateToTitle() fallback with Chinese conversational prefix stripping
  • Lowered overlap threshold from 0.7 to 0.5 for prompt matching
  • Guarded text block path against prompt-as-title

Changed Files

  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts

Summary by CodeRabbit

  • New Features

    • Improved session title generation with smarter reuse, AI-backed title creation, and more reliable fallback logic.
    • Enhanced Claude streaming support with partial content updates for more responsive output.
  • Bug Fixes

    • Preserved existing session summaries when incoming titles/summaries are empty or still the default placeholder.
    • Improved handling of task-related prompt context during message normalization.
    • Reduced noisy console logging during streaming and session reconnect events.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Anthropic-backed session title generation and JSONL title parsing, updates Claude SDK streaming to capture Task subagent prompts, and preserves existing project summaries when incoming values are placeholders.

Changes

AI Session Title and Streaming Updates

Layer / File(s) Summary
Anthropic title generation and parsing
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
resolveAnthropicConfig() resolves Anthropic credentials from env, settings, and credentials files; generateAiTitle() calls /v1/messages and falls back through thinking extraction and truncation; extractTitleFromThinking(), isPromptMatch(), and truncateToTitle() post-process title candidates.
JSONL title sources and reuse logic
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
extractLastPrompt() reads the latest last-prompt entry from JSONL; extractSessionAiTitleFromEnd() returns { title, kind } for custom-title and ai-title records; processSessionFile() switches to shouldSkip-based reuse and falls back through nameMap, JSONL extraction, and AI generation.
Task prompt capture in Claude SDK
server/claude-sdk.js
extractSubagentPrompt() parses Task tool_use inputs from object and JSON string forms; queryClaudeSDK() enables partial messages, collects Task prompts in a per-query Set, passes that context into normalizeMessage(), and removes startup, abort, and reconnect console logs.
Existing session summary guard
src/hooks/useProjectsState.ts
upsertSessionIntoProject() preserves an existing non-empty summary when the incoming summary or name is missing or equals the Untitled Claude Session placeholder.

Possibly related PRs

  • siteboon/claudecodeui#208: Extends the same server/claude-sdk.js streaming path that this PR updates with Task subagent prompt capture.
  • siteboon/claudecodeui#738: Touches the Claude session synchronizer title extraction flow that this PR reworks in server/modules/providers/list/claude/claude-session-synchronizer.provider.ts.

Suggested reviewers

  • blackmammoth

Poem

🐇 I hopped through titles, prompt by prompt,
And caught each Task tool in a prompty romp.
With streaming streams and summaries kept neat,
No placeholder fog could trick my treat.
Hop hop—now session names feel sweet!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fixing AI session auto-renaming title extraction.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/claude-sdk.js`:
- Around line 59-60: The JSON parsing guard in claude-sdk.js is too loose
because JSON.parse('null') still passes the object check and then reading
parsed.prompt can throw. Update the parsing path around the parsed variable to
explicitly reject null before accessing prompt, then keep the existing prompt
extraction logic so the SDK stream safely returns null instead of crashing.
- Around line 754-759: `sessionsService.normalizeMessage` is being called with
`streamingSubagentPrompts`, but the service and Claude provider signatures still
only accept three arguments, so the prompt filter data is dropped. Update the
`normalizeMessage` method in the sessions service, the provider interface, and
the Claude provider implementation to accept the optional fourth parameter and
forward it through to the normalization logic. Make sure the
`streamingSubagentPrompts` Set is passed all the way through so Task prompt
filtering can run during normalization.

In
`@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`:
- Around line 393-398: The auto-title logging in the session synchronizer is
exposing user prompt text and generated titles, so update the
`claude-session-synchronizer.provider.ts` flow inside the `if (!sessionName)`
block to stop logging sensitive contents. Keep the `console.info` messages in
place if needed, but remove the `prompt` and `title` payloads from the logs in
the `extractLastPrompt` and `generateAiTitle` path, and only log non-sensitive
context such as the session ID.
- Around line 289-296: The prompt-echo check in
claude-session-synchronizer.provider.ts is too aggressive because p.includes(c)
treats a valid title as invalid when it appears inside the prompt. Update the
matching logic in the title-validation path to only reject obvious prompt echoes
or near-duplicates, and avoid using a plain substring containment test against
the full prompt; keep the existing heuristics in the surrounding comparison
function but make the decision in this block rely on stronger signals than
simple inclusion.
- Around line 232-263: The early decision extraction paths in the title parsing
logic can still return prompt-echoed text because they bypass the prompt guard
used later in the same method. Update the candidate handling in the
`claude-session-synchronizer.provider.ts` title extraction flow so every matched
value from the decision and backtick patterns is checked with `isPromptMatch`
against `userPrompt` before returning. Keep the existing fallback behavior in
the relevant parsing routine, but make the guard apply consistently across all
candidate sources, including the initial `decisionPatterns` and
`backtickPatterns` branches.
- Around line 363-371: The skip logic in the Claude session synchronizer is
treating any custom session name longer than 60 characters as eligible for
regeneration, which can overwrite valid user-defined titles. Update the guard in
the session-name handling flow around the existingSessionName check so it
preserves custom names up to the full limit supported by normalizeSessionName
(60 should not be the cutoff), and keep the prompt-derived-name detection using
extractLastPrompt intact. Ensure the decision in ClaudeSessionSynchronizer
reflects “skip unless the name is prompt-derived,” not “skip only when the name
is short.”
- Around line 458-463: The custom-title handling in
claude-session-synchronizer.provider.ts should stop searching once the latest
title is the placeholder, instead of continuing backward and potentially
returning an older stale title. Update the title-scanning logic around the
custom-title branch in the session sync flow so that when claudeRenamedTitle is
"Untitled Claude Session" it is treated as no title and the search terminates
there, allowing AI generation to proceed rather than falling back to earlier
titles.
- Around line 119-133: The auth handling in resolveAnthropicConfig is collapsing
ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN into a single key, which loses
whether the request should use x-api-key or Authorization: Bearer. Update the
config flow in claude-session-synchronizer.provider.ts so it preserves the
credential kind alongside the value, and make the Anthropic request builder
choose the correct header based on that kind. Keep the env and settings.json
reading logic in place, but ensure the returned config from
resolveAnthropicConfig and the caller that sends the Messages request both use
the credential type explicitly.

In `@src/hooks/useProjectsState.ts`:
- Around line 260-267: The downgrade path in useProjectsState currently restores
only summary when normalizedSession has a degraded title, but name can still be
overwritten by the incoming “Untitled Claude Session”. Update the
isDegradedSummary branch so it preserves both session.summary and session.name
from the existing session before/after spreading normalizedSession, using the
existing session and updated object logic in this hook.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4e04fa80-a9ec-41a7-8ee3-8f2994fe0139

📥 Commits

Reviewing files that changed from the base of the PR and between 2ebe64f and 2e1a3bc.

📒 Files selected for processing (3)
  • server/claude-sdk.js
  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
  • src/hooks/useProjectsState.ts

Comment thread server/claude-sdk.js
Comment on lines +59 to +60
if (typeof parsed !== 'object') return null;
const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard JSON null before reading prompt.

JSON.parse('null') passes the typeof parsed === 'object' check, then parsed.prompt throws and can terminate the SDK stream.

Proposed fix
-  if (typeof parsed !== 'object') return null;
+  if (typeof parsed !== 'object' || parsed === null) return null;
   const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof parsed !== 'object') return null;
const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : null;
if (typeof parsed !== 'object' || parsed === null) return null;
const prompt = typeof parsed.prompt === 'string' ? parsed.prompt : null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/claude-sdk.js` around lines 59 - 60, The JSON parsing guard in
claude-sdk.js is too loose because JSON.parse('null') still passes the object
check and then reading parsed.prompt can throw. Update the parsing path around
the parsed variable to explicitly reject null before accessing prompt, then keep
the existing prompt extraction logic so the SDK stream safely returns null
instead of crashing.

Comment thread server/claude-sdk.js
Comment on lines +119 to +133
if (process.env.ANTHROPIC_API_KEY?.trim()) key = process.env.ANTHROPIC_API_KEY.trim();
if (process.env.ANTHROPIC_AUTH_TOKEN?.trim()) key = process.env.ANTHROPIC_AUTH_TOKEN.trim();
if (process.env.ANTHROPIC_BASE_URL?.trim()) baseUrl = process.env.ANTHROPIC_BASE_URL.trim();

// Read from ~/.claude/settings.json env block
try {
const settingsPath = path.join(this.claudeHome, 'settings.json');
const content = await readFile(settingsPath, 'utf8');
const settings: any = JSON.parse(content);
const env = settings?.env;
if (typeof env === 'object' && env) {
if (typeof env.ANTHROPIC_API_KEY === 'string' && env.ANTHROPIC_API_KEY.trim()) key = env.ANTHROPIC_API_KEY.trim();
if (typeof env.ANTHROPIC_AUTH_TOKEN === 'string' && env.ANTHROPIC_AUTH_TOKEN.trim()) key = env.ANTHROPIC_AUTH_TOKEN.trim();
if (typeof env.ANTHROPIC_BASE_URL === 'string' && env.ANTHROPIC_BASE_URL.trim()) baseUrl = env.ANTHROPIC_BASE_URL.trim();
if (typeof env.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string' && env.ANTHROPIC_DEFAULT_HAIKU_MODEL.trim()) model = env.ANTHROPIC_DEFAULT_HAIKU_MODEL.trim();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- outline ---'
ast-grep outline server/modules/providers/list/claude/claude-session-synchronizer.provider.ts --view expanded || true

echo '--- relevant lines ---'
nl -ba server/modules/providers/list/claude/claude-session-synchronizer.provider.ts | sed -n '100,190p'

echo '--- search x-api-key / ANTHROPIC_AUTH_TOKEN ---'
rg -n "x-api-key|ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|Authorization: \`Bearer|authHeader" server/modules/providers server/modules -g'*.ts' -g'*.js' || true

Repository: siteboon/claudecodeui

Length of output: 1360


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- relevant lines 113-190 ---'
sed -n '113,190p' server/modules/providers/list/claude/claude-session-synchronizer.provider.ts

echo '--- relevant lines 190-250 ---'
sed -n '190,250p' server/modules/providers/list/claude/claude-session-synchronizer.provider.ts

echo '--- other Anthropic-related usages ---'
rg -n "resolveAnthropicConfig|generateAiTitle|x-api-key|ANTHROPIC_API_KEY|ANTHROPIC_AUTH_TOKEN|Authorization: \`Bearer" server -g'*.ts' -g'*.js' || true

Repository: siteboon/claudecodeui

Length of output: 9144


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- claude-auth.provider.ts relevant lines ---'
sed -n '1,160p' server/modules/providers/list/claude/claude-auth.provider.ts

echo '--- opencode-auth.provider.ts relevant lines ---'
sed -n '1,120p' server/modules/providers/list/opencode/opencode-auth.provider.ts

echo '--- search credential handling in claude files ---'
rg -n "ANTHROPIC_AUTH_TOKEN|ANTHROPIC_API_KEY|x-api-key|Authorization: \`Bearer|Bearer \${.*key" server/modules/providers/list/claude -g'*.ts' || true

Repository: siteboon/claudecodeui

Length of output: 8638


🌐 Web query:

Anthropic Messages API authentication headers x-api-key Authorization Bearer official docs ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN

💡 Result:

The Anthropic Messages API supports two primary authentication methods: standard API keys and Workload Identity Federation (WIF) [1][2]. Authentication Headers: - x-api-key: Used for static API keys (typically starting with sk-ant-api...) [1]. This header is the standard way to provide your API key for authentication [2][3]. - Authorization: Bearer : Used for short-lived access tokens obtained through Workload Identity Federation [1][2]. Requirements: All API requests must include one of the two authentication headers listed above [2]. Additionally, all requests must include the following mandatory headers: - anthropic-version: Specifies the API version (e.g., 2023-06-01) [2][3]. - content-type: Must be set to application/json [2][3]. SDK Environment Variables: - ANTHROPIC_API_KEY: The official SDKs (Python, TypeScript, etc.) automatically detect and use this environment variable to populate the x-api-key header [1][2][4]. - ANTHROPIC_AUTH_TOKEN: Used in some SDK implementations (or custom integrations) to populate the Authorization: Bearer header [5][6]. If you are using the official Anthropic SDKs, they manage these headers automatically, so you generally do not need to construct them manually [1][2]. When making direct HTTP requests (e.g., via curl), ensure you provide the x-api-key header along with the required version and content-type headers [3]. Note that while some OAuth-related tokens may have historically supported Bearer authentication, current guidance emphasizes the x-api-key header for standard API key-based access [1][7].

Citations:


Preserve Anthropic auth type in the request. Anthropic Messages expects x-api-key for ANTHROPIC_API_KEY and Authorization: Bearer for ANTHROPIC_AUTH_TOKEN/OAuth, so resolveAnthropicConfig should carry the credential kind instead of collapsing both into key; otherwise API-key-only installs will 401 and always fall back to truncation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
around lines 119 - 133, The auth handling in resolveAnthropicConfig is
collapsing ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN into a single key, which
loses whether the request should use x-api-key or Authorization: Bearer. Update
the config flow in claude-session-synchronizer.provider.ts so it preserves the
credential kind alongside the value, and make the Anthropic request builder
choose the correct header based on that kind. Keep the env and settings.json
reading logic in place, but ensure the returned config from
resolveAnthropicConfig and the caller that sends the Messages request both use
the credential type explicitly.

Comment on lines +289 to +296
// Direct substring check
if (c.length >= 4 && p.length >= 4 && (c.includes(p) || p.includes(c))) return true;
if (pClean.length >= 4 && (c.includes(pClean) || pClean.includes(c))) return true;
// One starts with the other
if (c.length >= 4 && p.length >= 4 && (c.startsWith(p) || p.startsWith(c))) return true;
// Shared prefix overlap
if (c.length + p.length > 0 && Math.min(c.length, p.length) / Math.max(c.length, p.length) > 0.5) {
if (c.includes(p.slice(0, 8)) || p.includes(c.slice(0, 8))) return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t reject valid titles just because they’re contained in the prompt.

A good title like Fix login bug is often a prefix/subphrase of Fix login bug in production...; p.includes(c) marks it as prompt echo and forces fallback truncation.

🐛 Proposed fix
     // Direct substring check
-    if (c.length >= 4 && p.length >= 4 && (c.includes(p) || p.includes(c))) return true;
-    if (pClean.length >= 4 && (c.includes(pClean) || pClean.includes(c))) return true;
+    const isNearFullOverlap = (left: string, right: string): boolean => {
+      const shorter = Math.min(left.length, right.length);
+      const longer = Math.max(left.length, right.length);
+      return shorter >= 4 && longer > 0 && shorter / longer >= 0.8 && (left.includes(right) || right.includes(left));
+    };
+    if (isNearFullOverlap(c, p)) return true;
+    if (isNearFullOverlap(c, pClean)) return true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Direct substring check
if (c.length >= 4 && p.length >= 4 && (c.includes(p) || p.includes(c))) return true;
if (pClean.length >= 4 && (c.includes(pClean) || pClean.includes(c))) return true;
// One starts with the other
if (c.length >= 4 && p.length >= 4 && (c.startsWith(p) || p.startsWith(c))) return true;
// Shared prefix overlap
if (c.length + p.length > 0 && Math.min(c.length, p.length) / Math.max(c.length, p.length) > 0.5) {
if (c.includes(p.slice(0, 8)) || p.includes(c.slice(0, 8))) return true;
// Direct substring check
const isNearFullOverlap = (left: string, right: string): boolean => {
const shorter = Math.min(left.length, right.length);
const longer = Math.max(left.length, right.length);
return shorter >= 4 && longer > 0 && shorter / longer >= 0.8 && (left.includes(right) || right.includes(left));
};
if (isNearFullOverlap(c, p)) return true;
if (isNearFullOverlap(c, pClean)) return true;
// One starts with the other
if (c.length >= 4 && p.length >= 4 && (c.startsWith(p) || p.startsWith(c))) return true;
// Shared prefix overlap
if (c.length + p.length > 0 && Math.min(c.length, p.length) / Math.max(c.length, p.length) > 0.5) {
if (c.includes(p.slice(0, 8)) || p.includes(c.slice(0, 8))) return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
around lines 289 - 296, The prompt-echo check in
claude-session-synchronizer.provider.ts is too aggressive because p.includes(c)
treats a valid title as invalid when it appears inside the prompt. Update the
matching logic in the title-validation path to only reject obvious prompt echoes
or near-duplicates, and avoid using a plain substring containment test against
the full prompt; keep the existing heuristics in the surrounding comparison
function but make the decision in this block rely on stronger signals than
simple inclusion.

Comment on lines +363 to +371
if (existingSessionName && existingSessionName !== 'Untitled Claude Session' && existingSessionName.length <= 60) {
// Check if the existing name is just the raw prompt or a prefix of it
const lastPrompt = await this.extractLastPrompt(filePath);
if (lastPrompt && (existingSessionName === lastPrompt || lastPrompt.startsWith(existingSessionName))) {
// Existing name is derived from the prompt, not AI generated — regenerate
shouldSkip = false;
} else {
shouldSkip = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve real custom names longer than 60 characters.

Line 363 only skips custom names <= 60, so a user’s valid long custom title can be regenerated/overwritten even though normalizeSessionName supports up to 120 chars.

🐛 Proposed fix
-    if (existingSessionName && existingSessionName !== 'Untitled Claude Session' && existingSessionName.length <= 60) {
+    if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
       // Check if the existing name is just the raw prompt or a prefix of it
       const lastPrompt = await this.extractLastPrompt(filePath);
-      if (lastPrompt && (existingSessionName === lastPrompt || lastPrompt.startsWith(existingSessionName))) {
+      const trimmedPrompt = lastPrompt?.trim();
+      const trimmedExistingName = existingSessionName.trim();
+      if (
+        trimmedPrompt
+        && (
+          trimmedExistingName === trimmedPrompt
+          || (trimmedExistingName.length >= 60 && trimmedPrompt.startsWith(trimmedExistingName))
+        )
+      ) {
         // Existing name is derived from the prompt, not AI generated — regenerate
         shouldSkip = false;
       } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (existingSessionName && existingSessionName !== 'Untitled Claude Session' && existingSessionName.length <= 60) {
// Check if the existing name is just the raw prompt or a prefix of it
const lastPrompt = await this.extractLastPrompt(filePath);
if (lastPrompt && (existingSessionName === lastPrompt || lastPrompt.startsWith(existingSessionName))) {
// Existing name is derived from the prompt, not AI generated — regenerate
shouldSkip = false;
} else {
shouldSkip = true;
}
if (existingSessionName && existingSessionName !== 'Untitled Claude Session') {
// Check if the existing name is just the raw prompt or a prefix of it
const lastPrompt = await this.extractLastPrompt(filePath);
const trimmedPrompt = lastPrompt?.trim();
const trimmedExistingName = existingSessionName.trim();
if (
trimmedPrompt
&& (
trimmedExistingName === trimmedPrompt
|| (trimmedExistingName.length >= 60 && trimmedPrompt.startsWith(trimmedExistingName))
)
) {
// Existing name is derived from the prompt, not AI generated — regenerate
shouldSkip = false;
} else {
shouldSkip = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
around lines 363 - 371, The skip logic in the Claude session synchronizer is
treating any custom session name longer than 60 characters as eligible for
regeneration, which can overwrite valid user-defined titles. Update the guard in
the session-name handling flow around the existingSessionName check so it
preserves custom names up to the full limit supported by normalizeSessionName
(60 should not be the cutoff), and keep the prompt-derived-name detection using
extractLastPrompt intact. Ensure the decision in ClaudeSessionSynchronizer
reflects “skip unless the name is prompt-derived,” not “skip only when the name
is short.”

Comment on lines +393 to +398
if (!sessionName) {
const lastPrompt = await this.extractLastPrompt(filePath);
if (lastPrompt) {
console.info(`[AutoTitle] Generating AI title for session ${parsed.sessionId}`, { prompt: lastPrompt.slice(0, 80) });
sessionName = await this.generateAiTitle(lastPrompt);
console.info(`[AutoTitle] Result:`, { title: sessionName, sessionId: parsed.sessionId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove prompt/title contents from logs.

These logs persist user prompt text and generated titles, which can contain secrets or PII.

🛡️ Proposed fix
       const lastPrompt = await this.extractLastPrompt(filePath);
       if (lastPrompt) {
-        console.info(`[AutoTitle] Generating AI title for session ${parsed.sessionId}`, { prompt: lastPrompt.slice(0, 80) });
+        console.debug(`[AutoTitle] Generating AI title for session ${parsed.sessionId}`);
         sessionName = await this.generateAiTitle(lastPrompt);
-        console.info(`[AutoTitle] Result:`, { title: sessionName, sessionId: parsed.sessionId });
+        console.debug(`[AutoTitle] Generated AI title`, { sessionId: parsed.sessionId, hasTitle: Boolean(sessionName) });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!sessionName) {
const lastPrompt = await this.extractLastPrompt(filePath);
if (lastPrompt) {
console.info(`[AutoTitle] Generating AI title for session ${parsed.sessionId}`, { prompt: lastPrompt.slice(0, 80) });
sessionName = await this.generateAiTitle(lastPrompt);
console.info(`[AutoTitle] Result:`, { title: sessionName, sessionId: parsed.sessionId });
if (!sessionName) {
const lastPrompt = await this.extractLastPrompt(filePath);
if (lastPrompt) {
console.debug(`[AutoTitle] Generating AI title for session ${parsed.sessionId}`);
sessionName = await this.generateAiTitle(lastPrompt);
console.debug(`[AutoTitle] Generated AI title`, { sessionId: parsed.sessionId, hasTitle: Boolean(sessionName) });
}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 395-395: Avoid logging sensitive data
Context: console.info([AutoTitle] Generating AI title for session ${parsed.sessionId}, { prompt: lastPrompt.slice(0, 80) })
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)


[warning] 397-397: Avoid logging sensitive data
Context: console.info([AutoTitle] Result:, { title: sessionName, sessionId: parsed.sessionId })
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
around lines 393 - 398, The auto-title logging in the session synchronizer is
exposing user prompt text and generated titles, so update the
`claude-session-synchronizer.provider.ts` flow inside the `if (!sessionName)`
block to stop logging sensitive contents. Keep the `console.info` messages in
place if needed, but remove the `prompt` and `title` payloads from the logs in
the `extractLastPrompt` and `generateAiTitle` path, and only log non-sensitive
context such as the session ID.

Source: Linters/SAST tools

Comment on lines +458 to +463
if (eventType === 'custom-title' && claudeRenamedTitle?.trim()) {
// Ignore the default "Untitled Claude Session" placeholder — treat it
// as if there was no title at all so AI generation kicks in.
if (claudeRenamedTitle.trim() !== 'Untitled Claude Session') {
return { title: claudeRenamedTitle.trim(), kind: 'custom-title' };
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stop scanning after the latest placeholder title.

The comment says the placeholder should trigger AI generation, but the loop continues backward and can return an older stale title instead.

🐛 Proposed fix
         if (eventType === 'custom-title' && claudeRenamedTitle?.trim()) {
           // Ignore the default "Untitled Claude Session" placeholder — treat it
           // as if there was no title at all so AI generation kicks in.
-          if (claudeRenamedTitle.trim() !== 'Untitled Claude Session') {
-            return { title: claudeRenamedTitle.trim(), kind: 'custom-title' };
-          }
+          const trimmedTitle = claudeRenamedTitle.trim();
+          if (trimmedTitle === 'Untitled Claude Session') {
+            return undefined;
+          }
+          return { title: trimmedTitle, kind: 'custom-title' };
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (eventType === 'custom-title' && claudeRenamedTitle?.trim()) {
// Ignore the default "Untitled Claude Session" placeholder — treat it
// as if there was no title at all so AI generation kicks in.
if (claudeRenamedTitle.trim() !== 'Untitled Claude Session') {
return { title: claudeRenamedTitle.trim(), kind: 'custom-title' };
}
if (eventType === 'custom-title' && claudeRenamedTitle?.trim()) {
// Ignore the default "Untitled Claude Session" placeholder — treat it
// as if there was no title at all so AI generation kicks in.
const trimmedTitle = claudeRenamedTitle.trim();
if (trimmedTitle === 'Untitled Claude Session') {
return undefined;
}
return { title: trimmedTitle, kind: 'custom-title' };
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
around lines 458 - 463, The custom-title handling in
claude-session-synchronizer.provider.ts should stop searching once the latest
title is the placeholder, instead of continuing backward and potentially
returning an older stale title. Update the title-scanning logic around the
custom-title branch in the session sync flow so that when claudeRenamedTitle is
"Untitled Claude Session" it is treated as no title and the search terminates
there, allowing AI generation to proceed rather than falling back to earlier
titles.

Comment thread src/hooks/useProjectsState.ts
When the AI model quotes the user's message in its thinking block
(e.g. '- **User's message:** ...'), the title extraction mistakenly
used the user's own prompt as the session name.

Changes:
- Add isPromptMatch() to strip markdown prefixes and detect prompt echoes
- Add extractTitleFromThinking() for reasoning models (Qwen etc.)
- Add truncateToTitle() fallback with Chinese prefix stripping
- Lower overlap threshold from 0.7 to 0.5 for prompt matching
- Guard text block path against prompt-as-title

Fixes: siteboon#926, siteboon#935
Prevent backend 'Untitled Claude Session' or empty titles from overwriting
the user-set frontend title in the sidebar.
@WenhuaXia WenhuaXia force-pushed the feat/auto-rename-session-fix branch from 2e1a3bc to c3bf430 Compare June 30, 2026 16:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`:
- Line 116: The fallback model in claudeSessionSynchronizer is using an outdated
Claude Haiku ID, so update the default in the model selection logic to a current
Claude Haiku 4.5 value. In claude-session-synchronizer.provider.ts, adjust the
let model initialization to use the documented alias or pinned ID
(claude-haiku-4-5 or claude-haiku-4-5-20251001) so /v1/messages resolves to a
valid model.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0ac08c64-173f-4e8d-8662-594e8f09c8a9

📥 Commits

Reviewing files that changed from the base of the PR and between 2e1a3bc and c3bf430.

📒 Files selected for processing (3)
  • server/claude-sdk.js
  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
  • src/hooks/useProjectsState.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/hooks/useProjectsState.ts
  • server/claude-sdk.js

private async resolveAnthropicConfig(): Promise<{ key: string; baseUrl: string; model: string } | null> {
let key: string | null = null;
let baseUrl = 'https://api.anthropic.com';
let model = 'claude-haiku-4-20250915';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the provider around the default model and its callers/usages.
grep -nR --line-number --context=3 "claude-haiku-4" server/modules/providers/list/claude || true

echo
echo "=== file slice ==="
sed -n '90,150p' server/modules/providers/list/claude/claude-session-synchronizer.provider.ts

echo
echo "=== search for other Claude model defaults ==="
grep -R --line-number --context=2 "claude-haiku-4\|claude-sonnet\|claude-opus" server | head -200 || true

Repository: siteboon/claudecodeui

Length of output: 18683


🌐 Web query:

Anthropic Claude model IDs Haiku 4.5 claude-haiku-4-5-20251001 alias documentation current

💡 Result:

For Claude Haiku 4.5, the specific model ID and alias are as follows: - Claude API ID: claude-haiku-4-5-20251001 [1][2] - Claude API Alias: claude-haiku-4-5 [1][2] In the context of the Claude API, Claude Haiku 4.5 uses a dated model ID (claude-haiku-4-5-20251001) to represent a fixed, pinned snapshot of the model [3][1]. The alias (claude-haiku-4-5) acts as a convenience pointer that resolves to this specific dated version [1]. For other platforms, the identifiers differ to accommodate their specific naming requirements: - Amazon Bedrock: anthropic.claude-haiku-4-5-20251001-v1:0 [4][2] - Google Cloud Vertex AI: claude-haiku-4-5@20251001 [2] Anthropic documentation clarifies that for models released prior to the Claude 4.6 generation, such as Haiku 4.5, aliases serve as pointers to dated snapshots [3][1]. Starting with the 4.6 generation, model IDs transitioned to a dateless format which serves as the canonical, pinned snapshot ID itself [3].

Citations:


Use a current Claude Haiku default model. claude-haiku-4-20250915 is not a current Claude Haiku 4.5 ID; the documented alias is claude-haiku-4-5 and the pinned ID is claude-haiku-4-5-20251001. If this fallback is used, /v1/messages will target an unknown model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`
at line 116, The fallback model in claudeSessionSynchronizer is using an
outdated Claude Haiku ID, so update the default in the model selection logic to
a current Claude Haiku 4.5 value. In claude-session-synchronizer.provider.ts,
adjust the let model initialization to use the documented alias or pinned ID
(claude-haiku-4-5 or claude-haiku-4-5-20251001) so /v1/messages resolves to a
valid model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant