Skip to content

fix: session title priority — JSONL ai-title/custom-title over history.jsonl display - #982

Open
swu45 wants to merge 2 commits into
siteboon:mainfrom
swu45:fix/session-title-priority
Open

swu45 wants to merge 2 commits into
siteboon:mainfrom
swu45:fix/session-title-priority

Conversation

@swu45

@swu45 swu45 commented Jul 10, 2026

Copy link
Copy Markdown

Problem

CloudCLI sidebar shows the user's first typed message as the session title, instead of the AI-generated title that Claude Code produces. For example:

What Content
CloudCLI sidebar How to set up a React project with Tailwind CSS? (user's raw prompt)
Claude Code /resume React + Tailwind Project Setup Guide (AI-generated title)

Root cause — two bugs

Bug 1: processSessionFile checks history.jsonl before JSONL

history.jsonl's display field is a snapshot of user input—it was never a title. buildLookupMap uses first-write-wins, so nameMap.get() always returns the user's first message for CLI-started sessions. Since this check runs before extractSessionAiTitleFromEnd, the JSONL ai-title and custom-title events are never consulted.

Bug 2: extractSessionAiTitleFromEnd reverse-scan stops at last-prompt

The reverse-scan loop returns on the first match. Because last-prompt always appears at the end of the JSONL file, it is always the first match, shadowing ai-title and custom-title that appear earlier.

Fix

1. Swap JSONL and history.jsonl priority in processSessionFile

JSONL title extraction (extractSessionAiTitleFromEnd) now runs before the history.jsonl lookup, so AI-generated and /rename titles take precedence.

2. Full scan in extractSessionAiTitleFromEnd with proper priority

Changed from a reverse-scan-early-return to a full forward scan that collects all three title types separately, then returns them with the correct priority:

custom-title > ai-title > last-prompt

Final title resolution chain

custom_name (DB, user renamed via sidebar)
  → custom-title (JSONL, /rename in CLI)
    → ai-title (JSONL, Claude Code auto-generated)
      → last-prompt (JSONL, last user message)
        → history.jsonl display (first user message, fallback)
          → "Untitled Claude Session"

Files changed

File Change
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts Fix both bugs
server/modules/providers/tests/claude-sessions.test.ts 12 unit tests covering all title sources, priority, and edge cases
.gitignore Add issue/ for local tracking

Related

Summary by CodeRabbit

  • Bug Fixes
    • Improved Claude session title selection by first using transcript events (custom title, AI title, then last prompt).
    • Added a more reliable fallback to history-based titles when transcript data is missing.
    • Preserves existing meaningful custom names, while replacing default “Untitled” labels with better derived titles.
    • Ensures sessions with no usable title info consistently show “Untitled Claude Session.”

…y.jsonl display

Two bugs fixed:

1. processSessionFile: history.jsonl display was checked before JSONL
   extractSessionAiTitleFromEnd, so AI-generated titles (ai-title) and
   /rename titles (custom-title) were never used for CLI-started sessions.
   Swapped so JSONL is consulted first, with history.jsonl as fallback.

2. extractSessionAiTitleFromEnd: reverse-scan stopped at the first match,
   which was always last-prompt (at the end of the file), shadowing both
   ai-title and custom-title that appeared earlier. Changed to a full
   forward scan that collects all three types and returns them in priority
   order: custom-title > ai-title > last-prompt.

Final priority chain:
custom_name (DB) > custom-title (JSONL) > ai-title (JSONL)
> last-prompt (JSONL) > history.jsonl display > Untitled Claude Session

Added 12 unit tests covering all title sources and edge cases.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Claude session synchronization now prioritizes matching JSONL title events, then falls back to history and default names. New isolated tests cover lookup maps, title precedence, database overrides, and ignored transcript inputs.

Changes

Claude title synchronization

Layer / File(s) Summary
Title extraction and priority resolution
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
Matching JSONL events collect custom-title, ai-title, and last-prompt values, applying a fixed priority before history-derived names.
Synchronization behavior coverage
server/modules/providers/tests/claude-sessions.test.ts
Tests cover isolated filesystem and database setup, lookup-map behavior, title-source precedence, database overrides, default names, subagent transcripts, and non-JSONL inputs.

Sequence Diagram(s)

sequenceDiagram
  participant SessionFile
  participant ClaudeSessionSynchronizer
  participant History
  ClaudeSessionSynchronizer->>SessionFile: scan matching JSONL title events
  SessionFile-->>ClaudeSessionSynchronizer: title candidates
  ClaudeSessionSynchronizer->>History: read fallback nameMap when needed
  History-->>ClaudeSessionSynchronizer: display name
Loading

Possibly related PRs

Suggested reviewers: blackmammoth

Poem

A rabbit found titles tucked in JSONL bright,
Chose custom names first, then prompts in sight.
History waits when the stream is bare,
While tests guard each fallback with care.
Hop, hop—sessions now wear names just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: JSONL title fields now take priority over history.jsonl display values.
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.

🧹 Nitpick comments (4)
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts (2)

175-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Rename extractSessionAiTitleFromEnd to reflect its new behavior.

The function now scans forward (not from the end) and collects all three title types (custom-title, ai-title, last-prompt), not just AI titles. The name extractSessionAiTitleFromEnd is misleading to future maintainers. Consider something like extractSessionTitle or resolveSessionTitleFromTranscript.

🤖 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 175 - 178, Rename extractSessionAiTitleFromEnd to a name reflecting
forward transcript scanning and resolution of custom-title, ai-title, and
last-prompt values, such as extractSessionTitle or
resolveSessionTitleFromTranscript; update its declaration and every call site
consistently.

148-151: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

File is read twice in processSessionFile.

extractFirstValidJsonlData (line 117) streams the file for sessionId/cwd, then extractSessionAiTitleFromEnd (line 148) loads the entire file again via readFile. For large session transcripts this doubles I/O and loads the full content into memory. Consider merging both passes into a single scan, or at minimum using a streaming approach in extractSessionAiTitleFromEnd to avoid loading the entire file.

Also applies to: 180-180

🤖 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 148 - 151, Refactor processSessionFile and
extractSessionAiTitleFromEnd to avoid reading the session file twice and loading
it fully into memory: combine session metadata and AI-title extraction into one
streaming scan, or make extractSessionAiTitleFromEnd stream the file
incrementally. Preserve the existing fallback to nameMap when no title is found.
server/modules/providers/tests/claude-sessions.test.ts (2)

145-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a direct test for custom-title vs ai-title priority.

The current tests verify custom-title beats last-prompt and ai-title beats last-prompt, but no test has both custom-title and ai-title present to directly verify custom-title > ai-title. Adding this would close the last gap in the priority chain coverage.

Also applies to: 190-227

🤖 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/tests/claude-sessions.test.ts` around lines 145 -
188, The tests lack direct coverage for priority between custom-title and
ai-title. Add a test alongside the existing synchronizeFile priority tests that
writes both custom-title and ai-title records to the same session JSONL, runs
ClaudeSessionSynchronizer.synchronizeFile, and asserts the stored session
custom_name equals the custom-title value, verifying custom-title takes
precedence over ai-title.

271-307: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for last-prompt as the sole title source.

The priority chain includes last-prompt before history.jsonl display, but no test verifies this specific case. If last-prompt handling is accidentally removed, no existing test would catch it. A test where the JSONL has only a last-prompt event (no ai-title or custom-title) and history.jsonl has a competing display name would close this gap.

🧪 Suggested test
test('synchronizeFile uses last-prompt from JSONL when no custom-title or ai-title exists', { concurrency: false }, async () => {
  const tmp = await mkdtemp(path.join(os.tmpdir(), 'claude-sync-lastprompt-'));
  const workspacePath = path.join(tmp, 'workspace');
  await mkdir(workspacePath, { recursive: true });
  const restoreHomeDir = patchHomeDir(tmp);

  try {
    const claudeHome = path.join(tmp, '.claude');
    await mkdir(claudeHome, { recursive: true });
    await writeFile(
      path.join(claudeHome, 'history.jsonl'),
      JSON.stringify({ sessionId: 'test-session-1', display: 'history-display' }) + '\n',
      'utf8',
    );

    await writeSessionJsonl(workspacePath, 'test-session-1.jsonl', [
      JSON.stringify({ type: 'last-prompt', lastPrompt: 'My last prompt', sessionId: 'test-session-1' }),
    ]);

    await withIsolatedDatabase(async () => {
      const synchronizer = new ClaudeSessionSynchronizer();
      const result = await synchronizer.synchronizeFile(
        path.join(workspacePath, 'test-session-1.jsonl'),
      );

      assert.ok(result);
      const session = sessionsDb.getSessionById(result!);
      assert.equal(session?.custom_name, 'My last prompt');
    });
  } finally {
    restoreHomeDir();
    await rm(tmp, { recursive: true, force: true });
  }
});

Also applies to: 229-269

🤖 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/tests/claude-sessions.test.ts` around lines 271 -
307, Add a regression test alongside the existing synchronizeFile
title-selection tests covering a JSONL file containing only a last-prompt event,
with no ai-title or custom-title, while history.jsonl contains a competing
display name. Use ClaudeSessionSynchronizer.synchronizeFile and assert the
persisted session custom_name equals the last-prompt value, verifying that
last-prompt takes priority over history display.
🤖 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.

Nitpick comments:
In
`@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`:
- Around line 175-178: Rename extractSessionAiTitleFromEnd to a name reflecting
forward transcript scanning and resolution of custom-title, ai-title, and
last-prompt values, such as extractSessionTitle or
resolveSessionTitleFromTranscript; update its declaration and every call site
consistently.
- Around line 148-151: Refactor processSessionFile and
extractSessionAiTitleFromEnd to avoid reading the session file twice and loading
it fully into memory: combine session metadata and AI-title extraction into one
streaming scan, or make extractSessionAiTitleFromEnd stream the file
incrementally. Preserve the existing fallback to nameMap when no title is found.

In `@server/modules/providers/tests/claude-sessions.test.ts`:
- Around line 145-188: The tests lack direct coverage for priority between
custom-title and ai-title. Add a test alongside the existing synchronizeFile
priority tests that writes both custom-title and ai-title records to the same
session JSONL, runs ClaudeSessionSynchronizer.synchronizeFile, and asserts the
stored session custom_name equals the custom-title value, verifying custom-title
takes precedence over ai-title.
- Around line 271-307: Add a regression test alongside the existing
synchronizeFile title-selection tests covering a JSONL file containing only a
last-prompt event, with no ai-title or custom-title, while history.jsonl
contains a competing display name. Use ClaudeSessionSynchronizer.synchronizeFile
and assert the persisted session custom_name equals the last-prompt value,
verifying that last-prompt takes priority over history display.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e306b427-6796-4857-b9dc-c6e104c09fbf

📥 Commits

Reviewing files that changed from the base of the PR and between 5884573 and aff58da.

📒 Files selected for processing (2)
  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
  • server/modules/providers/tests/claude-sessions.test.ts

@swu45

swu45 commented Jul 10, 2026

Copy link
Copy Markdown
Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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

🧹 Nitpick comments (1)
server/modules/providers/list/claude/claude-session-synchronizer.provider.ts (1)

175-178: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Method name extractSessionAiTitleFromEnd is now misleading.

The implementation scans forward (line 187: for (let index = 0; index < lines.length; ...)), but the name still says "FromEnd." Consider renaming to extractSessionTitle or extractSessionTitleFromTranscript to match the forward-scan behavior and avoid confusing future maintainers.

🤖 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 175 - 178, Rename the misleading `extractSessionAiTitleFromEnd`
method to `extractSessionTitle` (or `extractSessionTitleFromTranscript`) to
reflect its forward transcript scan, and update every call site and related
references consistently.
🤖 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/tests/claude-sessions.test.ts`:
- Around line 141-143: Add a concurrency-disabled test near the existing
synchronizeFile title tests that creates a session JSONL containing only a
last-prompt event, optionally with a conflicting history.jsonl display, then
asserts synchronizeFile stores the lastPrompt value as custom_name. Also add or
extend a test containing custom-title, ai-title, and last-prompt events to
verify the complete custom-title > ai-title > last-prompt precedence chain,
using the existing test helpers and cleanup patterns.

---

Nitpick comments:
In
`@server/modules/providers/list/claude/claude-session-synchronizer.provider.ts`:
- Around line 175-178: Rename the misleading `extractSessionAiTitleFromEnd`
method to `extractSessionTitle` (or `extractSessionTitleFromTranscript`) to
reflect its forward transcript scan, and update every call site and related
references consistently.
🪄 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: 1431b534-85b3-4339-8636-3b47e8419ab7

📥 Commits

Reviewing files that changed from the base of the PR and between 5884573 and aff58da.

📒 Files selected for processing (2)
  • server/modules/providers/list/claude/claude-session-synchronizer.provider.ts
  • server/modules/providers/tests/claude-sessions.test.ts

Comment on lines +141 to +143
// ---------------------------------------------------------------------------
// extractSessionAiTitleFromEnd — tested via synchronizeFile
// ---------------------------------------------------------------------------

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

Missing test for last-prompt as the sole JSONL title source.

The last-prompt fallback within extractSessionAiTitleFromEnd is never exercised in isolation. In every test where last-prompt appears (lines 171, 210, 331, 373), a higher-priority event (ai-title or custom-title) is always present, so last-prompt never wins. There's also no test with all three event types present to verify the full custom-title > ai-title > last-prompt priority chain.

Consider adding a test where last-prompt is the only title event (optionally with a competing history.jsonl display) to verify it takes priority over history and produces the expected custom_name.

🧪 Suggested test for last-prompt fallback
test('synchronizeFile uses last-prompt from JSONL when no custom-title or ai-title exists', { concurrency: false }, async () => {
  const tmp = await mkdtemp(path.join(os.tmpdir(), 'claude-sync-lastprompt-'));
  const workspacePath = path.join(tmp, 'workspace');
  await mkdir(workspacePath, { recursive: true });
  const restoreHomeDir = patchHomeDir(tmp);

  try {
    const claudeHome = path.join(tmp, '.claude');
    await mkdir(claudeHome, { recursive: true });
    // Include a competing history.jsonl display to verify last-prompt wins over it.
    await writeFile(
      path.join(claudeHome, 'history.jsonl'),
      JSON.stringify({ sessionId: 'test-session-1', display: 'history-display' }) + '\n',
      'utf8',
    );

    await writeSessionJsonl(workspacePath, 'test-session-1.jsonl', [
      JSON.stringify({ type: 'last-prompt', lastPrompt: 'the final user prompt', sessionId: 'test-session-1' }),
    ]);

    await withIsolatedDatabase(async () => {
      const synchronizer = new ClaudeSessionSynchronizer();
      const result = await synchronizer.synchronizeFile(
        path.join(workspacePath, 'test-session-1.jsonl'),
      );

      assert.ok(result);
      const session = sessionsDb.getSessionById(result!);
      assert.equal(session?.custom_name, 'the final user prompt');
    });
  } finally {
    restoreHomeDir();
    await rm(tmp, { recursive: true, force: 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/tests/claude-sessions.test.ts` around lines 141 -
143, Add a concurrency-disabled test near the existing synchronizeFile title
tests that creates a session JSONL containing only a last-prompt event,
optionally with a conflicting history.jsonl display, then asserts
synchronizeFile stores the lastPrompt value as custom_name. Also add or extend a
test containing custom-title, ai-title, and last-prompt events to verify the
complete custom-title > ai-title > last-prompt precedence chain, using the
existing test helpers and cleanup patterns.

@aldredb

aldredb commented Sep 4, 2026

Copy link
Copy Markdown

I hit this bug independently and worked out the same cause before finding this PR, so here is a confirmation plus an offer of help.

Still reproducing. On the published 1.37.2, every Claude session in the sidebar shows its first prompt instead of its title. The reason is the one described here: history.jsonl is consulted before the transcript, and buildLookupMap keeps the first value it sees for a key. Since any session with at least one prompt appears in history.jsonl, extractSessionAiTitleFromEnd is effectively unreachable. I checked the file at ref=main today and both defects are unchanged, so this PR is not stale in substance — only in its merge base.

There is a third symptom worth recording. Claude Code writes custom-title immediately before ai-title, so even when the fallback is reached, a reverse scan returning its first hit will always prefer the auto-generated title over a manual /rename. The forward scan here fixes that too.

On the conflict. Only main's newer claude-sessions.test.ts conflicts; the provider file still merges cleanly. I rebased onto current main (c1be241), resolved it by keeping both disjoint test sets, and opened it against this branch as swu45#1 — merging that makes this PR mergeable again with @swu45's commits and authorship intact. It also renames extractSessionAiTitleFromEnd, which addresses the review comment above, as a separate commit that can be dropped.

After the rebase: 22/22 in that file, 407/409 in the full server suite, typecheck clean, no new lint warnings. The single failure is conversation search streams title matches before transcript results, which fails identically on unmodified main.

@swu45, are you still able to land this? If you would rather not, I am happy to open the rebased branch directly against main with your commits preserved — just say the word.

Two related things for whoever picks this up: #1178 rewrites the same function to bound the scan for performance, so these two will need sequencing; and #747 covers the custom-title half of this and was closed as completed, though the reporter said the next day that it still reproduced.

aldredb added a commit to aldredb/claudecodeui that referenced this pull request Sep 4, 2026
The function scans forward and returns a custom-title, ai-title or
last-prompt, so its name described neither its direction nor its result.
Addresses the review comment on siteboon#982.
@aldredb

aldredb commented Sep 4, 2026

Copy link
Copy Markdown

Following up on my comment above: I have opened the rebased branch directly against main as #1258, since this PR has been conflicting for a while.

@swu45's two commits are unchanged there and keep their authorship. swu45#1 is still open against this branch, so if @swu45 would rather land it here, merging that makes this PR mergeable again and #1258 can be closed. Either route is fine by me.

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.

2 participants