Skip to content

Clean up Claude session titles - #3211

Merged
lawrencecchen merged 7 commits into
mainfrom
task-prettier-claude-session-titles
Apr 28, 2026
Merged

lawrencecchen merged 7 commits into
mainfrom
task-prettier-claude-session-titles

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Skip Claude meta local-command caveats when choosing session titles.
  • Render Claude slash-command markup as readable command titles.
  • Add a localized fallback for cached local-command caveat titles.

Testing

  • jq empty Resources/Localizable.xcstrings
  • ./scripts/reload.sh --tag cctitle
  • Not run: local XCTest, per repo policy.

Issues

  • Related task: Claude Code sessions show raw <local-command-caveat> titles in the Sessions sidebar.

Summary by cubic

Cleaned up Claude session titles in the Sessions sidebar and made resume commands run in the correct config root. Session discovery now scans multiple Claude roots.

  • Bug Fixes

    • Ignore Claude meta/synthetic envelopes (e.g., <local-command-*>, <system-reminder>) and show localized “Local command” (en/ja); adds regression tests.
    • Prefix resume commands with env CLAUDE_CONFIG_DIR=<root> derived from the session file path so actions run in the right config; adds tests.
  • New Features

    • Render readable titles from Claude slash-command markup by combining <command-name>, distinct <command-message>, and <command-args> (e.g., "/plugin install codex@openai-codex").
    • Discover and search sessions across CLAUDE_CONFIG_DIR, validated ~/.codex-accounts/claude/*, and default ~/.claude.

Written for commit 2d959d3. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Improvements

    • Session titles now parse and surface structured Claude command details for clearer single-line display.
    • Resume actions for Claude sessions are prefixed with a session-specific config directory so resume commands run in the correct context.
    • Discovery now recognizes multiple Claude roots and deduplicates projects; synthetic/system sessions are shown as "Untitled" where appropriate.
  • Localization

    • Added "Local command" translation (English, Japanese).
  • Tests

    • Added tests for title parsing, local-command labeling, and resume command formatting.

@vercel

vercel Bot commented Apr 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 28, 2026 7:46am
cmux-staging Building Building Preview, Comment Apr 28, 2026 7:46am

@coderabbitai

coderabbitai Bot commented Apr 28, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c69b5fd-a486-4726-803d-ff35153d9b5e

📥 Commits

Reviewing files that changed from the base of the PR and between 4548060 and 2d959d3.

📒 Files selected for processing (2)
  • Sources/SessionIndexStore.swift
  • cmuxTests/SessionIndexViewTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxTests/SessionIndexViewTests.swift

📝 Walkthrough

Walkthrough

Refactors Claude session discovery and title handling and adds localized label for local commands. Session enumeration now scans multiple Claude config roots, applies per-candidate ripgrep prefiltering, and builds resume CLI invocations that inject a per-session CLAUDE_CONFIG_DIR. Tests and a localization string were added.

Changes

Cohort / File(s) Summary
Localization
Resources/Localizable.xcstrings
Adds sessionIndex.localCommand translations (en: "Local command", ja: "ローカルコマンド").
Indexer / Session logic
Sources/SessionIndexStore.swift
Major rewrite: enumerate multiple Claude config roots (CLAUDE_CONFIG_DIR, ~/.codex-accounts/claude/*, ~/.claude), per-root projects discovery, unified candidate model with per-candidate ripgrep prefilter flag, inject per-session CLAUDE_CONFIG_DIR into claude --resume commands (shell-quoted env assignment), parse/refactor displayTitle to prefer slash-command envelopes and map <local-command-...> to localized label, and suppress metadata-marked lines via isMeta.
Tests
cmuxTests/SessionIndexViewTests.swift
Adds XCTest coverage for displayTitle parsing (slash-command, message+args, meta suppression, local-command mapping) and for resumeCommand CLAUDE_CONFIG_DIR selection; introduces makeEntry test helper for deterministic SessionEntry creation.

Sequence Diagram(s)

sequenceDiagram
  participant Indexer as SessionIndexStore
  participant FS as FileSystem
  participant RG as Ripgrep
  participant Parser as TitleParser
  participant Shell as CLI Builder
  participant L10n as Localizable

  Indexer->>FS: enumerate roots (env, ~/.codex-accounts/claude/*, ~/.claude)
  FS-->>Indexer: list of project candidate dirs
  Indexer->>RG: prefilter candidates (cwdFilter / ripgrep)
  RG-->>Indexer: prefilter result per-candidate
  Indexer->>Parser: extract claude metadata & isMeta flags
  Parser-->>Indexer: parsed title components (command-name/message/args)
  Indexer->>L10n: request "Local command" translation for local-command envelopes
  L10n-->>Indexer: localized label
  Indexer->>Shell: build resume command with env CLAUDE_CONFIG_DIR (shell-quoted)
  Shell-->>Indexer: resume command string
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

"I hop through roots and ripgrep trails,
I stitch a title from slash-command tales,
With CLAUDE_CONFIG_DIR snug in a shelly coat,
I whisper 'Local command' when a local note! 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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
Title check ✅ Passed The title 'Clean up Claude session titles' directly and clearly summarizes the main change: improving how Claude session titles are displayed in the sidebar by filtering meta envelopes and rendering slash-command markup.
Description check ✅ Passed The description includes all required template sections: Summary (what changed and why), Testing (verification steps performed), and a Checklist with completion status indicated.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-prettier-claude-session-titles

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR filters out Claude's synthetic markup envelopes (<local-command-caveat>, <system-reminder>) from session titles in the Sessions sidebar, and renders slash-command XML tags as readable command titles (e.g. /plugin install codex@openai-codex). A new sessionIndex.localCommand localization key (English + Japanese) provides a human-readable fallback for older sessions where the raw <local-command-caveat> envelope was previously stored as the title.

Confidence Score: 5/5

Safe to merge — logic is correct, regression tests follow the two-commit policy, and all new user-facing strings are properly localized.

No P0 or P1 issues found. The title-parsing helpers handle all edge cases correctly (empty strings, synthetic envelopes, nested tag absence, isMeta scoped per JSONL line). The "Local command" path in displayTitle is intentionally a backward-compat escape hatch for cached sessions (per the PR description), not a bug. Localization is complete for both supported languages.

No files require special attention.

Important Files Changed

Filename Overview
Sources/SessionIndexStore.swift Adds Claude-specific title parsing helpers and wires isMeta flag into the session-head extraction loop; logic is correct and handles all cases cleanly.
cmuxTests/SessionIndexViewTests.swift Adds two regression tests and a makeEntry helper covering the local-command-caveat backward-compat path and slash-command markup rendering; tests verify observable runtime behavior through displayTitle.
Resources/Localizable.xcstrings Adds sessionIndex.localCommand with English ("Local command") and Japanese ("ローカルコマンド") translations; follows existing localization conventions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[displayTitle called] --> B{agent == .claude?}
    B -- No --> Z[generic empty/trimmed fallback]
    B -- Yes --> C[claudeDisplayTitle from trimmed]
    C --> D{Returns non-nil?}
    D -- Yes --> E[Return cleaned title\ne.g. /plugin install args]
    D -- No --> F{isClaudeLocalCommandEnvelope?}
    F -- Yes --> G[Return 'Local command'\n sessionIndex.localCommand]
    F -- No --> H{isClaudeSyntheticEnvelope?\ne.g. system-reminder}
    H -- Yes --> I[Return 'Untitled session'\n sessionIndex.untitled]
    H -- No --> Z
    Z --> J{trimmed.isEmpty?}
    J -- Yes --> I
    J -- No --> K[Return trimmed]

    subgraph claudeDisplayTitle
        L[Trim whitespace] --> M{Empty?}
        M -- Yes --> N[return nil]
        M -- No --> O[claudeSlashCommandTitle?]
        O -- Found --> P[Return /cmd args]
        O -- Not found --> Q{isMeta or SyntheticEnvelope?}
        Q -- Yes --> N
        Q -- No --> R[Return trimmed text]
    end
Loading

Reviews (1): Last reviewed commit: "Clean up Claude session titles" | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmuxTests/SessionIndexViewTests.swift`:
- Around line 14-20: The test testClaudeLocalCommandCaveatDisplaysReadableTitle
hardcodes the English string "Local command" while entry.displayTitle is
produced from String(localized:), making the assertion locale-sensitive; update
the assertion to compare entry.displayTitle against the same localization lookup
used by the production code (e.g., use String(localized: <the displayTitle's
localization key> or the same NSLocalizedString key/initializer the displayTitle
implementation uses) rather than the English literal, ensuring the test remains
correct across locales and referencing displayTitle and makeEntry to locate the
relevant code.

In `@Sources/SessionIndexStore.swift`:
- Around line 130-137: The function claudeDisplayTitle(from:isMeta:) checks for
slash-command titles before honoring the isMeta flag, so move the isMeta (and
isClaudeSyntheticEnvelope) check to occur before calling
claudeSlashCommandTitle; specifically, in claudeDisplayTitle ensure you
immediately return nil when isMeta is true (or when
isClaudeSyntheticEnvelope(trimmed) is true) and only then attempt to extract a
claudeSlashCommandTitle from trimmed text.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 770b125d-39c1-4227-91da-3ff4df9fd1d3

📥 Commits

Reviewing files that changed from the base of the PR and between 259de55 and f92c39c.

📒 Files selected for processing (3)
  • Resources/Localizable.xcstrings
  • Sources/SessionIndexStore.swift
  • cmuxTests/SessionIndexViewTests.swift

Comment thread cmuxTests/SessionIndexViewTests.swift
Comment thread Sources/SessionIndexStore.swift

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a1bce5e60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/SessionIndexStore.swift
Comment thread Sources/SessionIndexStore.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/SessionIndexStore.swift (1)

159-166: ⚠️ Potential issue | 🟠 Major

Honor isMeta/synthetic guards before slash-command parsing.

Line 162 still returns claudeSlashCommandTitle(...) before Line 165 checks isMeta and synthetic envelopes, so meta <local-command-*> / <system-reminder> payloads can still become sidebar titles instead of being skipped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionIndexStore.swift` around lines 159 - 166, In
claudeDisplayTitle(from:isMeta:), the function calls
claudeSlashCommandTitle(from:) before checking isMeta or
isClaudeSyntheticEnvelope, allowing meta/synthetic payloads to be treated as
slash-command titles; reorder the logic so the guard for isMeta ||
isClaudeSyntheticEnvelope(trimmed) runs before attempting
claudeSlashCommandTitle, returning nil for meta/synthetic envelopes and only
calling claudeSlashCommandTitle when those guards pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 1308-1372: loadClaudeEntries currently skips an entire root when
ripgrepMatchingPaths returns nil for that root, causing incomplete results for
non-empty needles; modify the needle branch so that for each root, if
ripgrepMatchingPaths returns nil you fall back to the same directory enumeration
used in the else branch (enumerate root.projectsRoot project dirs or the
specific cwd-encoded dir and append *.jsonl files to candidates), and only set
rgFiltered = true when ripgrepMatchingPaths actually returns results; reference
loadClaudeEntries, ripgrepMatchingPaths, candidates and rgFiltered when making
this change.

---

Duplicate comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 159-166: In claudeDisplayTitle(from:isMeta:), the function calls
claudeSlashCommandTitle(from:) before checking isMeta or
isClaudeSyntheticEnvelope, allowing meta/synthetic payloads to be treated as
slash-command titles; reorder the logic so the guard for isMeta ||
isClaudeSyntheticEnvelope(trimmed) runs before attempting
claudeSlashCommandTitle, returning nil for meta/synthetic envelopes and only
calling claudeSlashCommandTitle when those guards pass.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b867149-db0f-4ea9-9072-a425bbdf57c8

📥 Commits

Reviewing files that changed from the base of the PR and between f92c39c and 4a1bce5.

📒 Files selected for processing (2)
  • Sources/SessionIndexStore.swift
  • cmuxTests/SessionIndexViewTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmuxTests/SessionIndexViewTests.swift

Comment thread Sources/SessionIndexStore.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
Sources/SessionIndexStore.swift (2)

159-166: ⚠️ Potential issue | 🟠 Major

Reject meta and synthetic envelopes before slash-command parsing.

claudeSlashCommandTitle(...) still runs before the isMeta / synthetic-envelope check, so meta user records and <local-command-*> / <system-reminder> envelopes can still win the session title and bypass the localized fallback.

Suggested fix
     static func claudeDisplayTitle(from raw: String, isMeta: Bool = false) -> String? {
         let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
         guard !trimmed.isEmpty else { return nil }
-        if let commandTitle = claudeSlashCommandTitle(from: trimmed) {
-            return commandTitle
-        }
         if isMeta || isClaudeSyntheticEnvelope(trimmed) {
             return nil
         }
+        if let commandTitle = claudeSlashCommandTitle(from: trimmed) {
+            return commandTitle
+        }
         return trimmed
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionIndexStore.swift` around lines 159 - 166, In
claudeDisplayTitle(from:isMeta:), reject meta or synthetic envelopes before
attempting slash-command parsing: first trim and guard non-empty, then check if
isMeta || isClaudeSyntheticEnvelope(trimmed) and return nil if true, and only
after that call claudeSlashCommandTitle(from:) so meta user records and
<local-command-*> / <system-reminder> envelopes cannot win the session title.

1320-1336: ⚠️ Potential issue | 🟠 Major

Keep the non-rg fallback on a per-root basis.

When ripgrepMatchingPaths(...) returns nil for one Claude root, this branch just continues and drops that root entirely. That means non-empty Claude searches can be partial, and if every root hits nil they return no results instead of falling back to the directory/head/tail scan.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/SessionIndexStore.swift` around lines 1320 - 1336, The loop currently
drops an entire Claude root when ripgrepMatchingPaths(...) returns nil (the
guard uses continue), causing the non-rg fallback to be skipped for that root;
instead, when ripgrepMatchingPaths returns nil for a given root you should not
continue but invoke the non-rg fallback scan for that root (the same
directory/head/tail scan that populates candidates when ripgrep is unavailable),
leaving rgFiltered unset for that root; update the for root in roots loop to
branch: if ripgrepMatchingPaths(...) yields paths use them to append candidates
(setting rgFiltered = true), otherwise run the existing fallback logic that
inspects files, uses fm.attributesOfItem(atPath:), extracts mtime and dirName
via claudeProjectDirName(for:projectsRoot:) and appends to candidates so each
root is independently handled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 105-114: The code in claudeConfigDirectoryForResume uses
fileURL.pathComponents.firstIndex(of: "projects") which picks the first
"projects" component and breaks custom Claude roots; change this to find the
last occurrence (use fileURL.pathComponents.lastIndex(of: "projects") or
lastIndex(where:) ) so projectsIndex refers to the final "projects" segment,
then reconstruct configComponents and configDir the same way and keep the
existing empty-check logic.
- Around line 171-179: The claudeSlashCommandTitle function currently selects
either "command-name" or "command-message" and drops the other; change it to
read both via claudeTagValue("command-name", in: raw) and
claudeTagValue("command-message", in: raw) and concatenate them in the intended
order (command-name followed by command-message) when both exist, and then
append command-args if present; update claudeSlashCommandTitle to return the
joined components (skipping any nil/empty pieces) so a payload with all three
tags renders "command-name command-message command-args".

---

Duplicate comments:
In `@Sources/SessionIndexStore.swift`:
- Around line 159-166: In claudeDisplayTitle(from:isMeta:), reject meta or
synthetic envelopes before attempting slash-command parsing: first trim and
guard non-empty, then check if isMeta || isClaudeSyntheticEnvelope(trimmed) and
return nil if true, and only after that call claudeSlashCommandTitle(from:) so
meta user records and <local-command-*> / <system-reminder> envelopes cannot win
the session title.
- Around line 1320-1336: The loop currently drops an entire Claude root when
ripgrepMatchingPaths(...) returns nil (the guard uses continue), causing the
non-rg fallback to be skipped for that root; instead, when ripgrepMatchingPaths
returns nil for a given root you should not continue but invoke the non-rg
fallback scan for that root (the same directory/head/tail scan that populates
candidates when ripgrep is unavailable), leaving rgFiltered unset for that root;
update the for root in roots loop to branch: if ripgrepMatchingPaths(...) yields
paths use them to append candidates (setting rgFiltered = true), otherwise run
the existing fallback logic that inspects files, uses
fm.attributesOfItem(atPath:), extracts mtime and dirName via
claudeProjectDirName(for:projectsRoot:) and appends to candidates so each root
is independently handled.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7128b8be-4192-4004-921f-a99a806a169b

📥 Commits

Reviewing files that changed from the base of the PR and between 4a1bce5 and 4548060.

📒 Files selected for processing (1)
  • Sources/SessionIndexStore.swift

Comment thread Sources/SessionIndexStore.swift
Comment thread Sources/SessionIndexStore.swift Outdated

This branch was successfully deployed

1 active deployment
Preview – cmux — 2d959d31 Deployed Apr 28, 2026 by vercel[bot]
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