Skip to content

feat: auto-name new sessions and support API-key renames#643

Open
walksoda wants to merge 1 commit intositeboon:mainfrom
walksoda:pr/session-auto-naming
Open

feat: auto-name new sessions and support API-key renames#643
walksoda wants to merge 1 commit intositeboon:mainfrom
walksoda:pr/session-auto-naming

Conversation

@walksoda
Copy link
Copy Markdown

@walksoda walksoda commented Apr 11, 2026

Summary

Today, newly created Claude sessions are labelled by a truncated slice of the first user message. Once the sidebar fills up with dozens of sessions that all start with "help me fix...", it gets hard to tell them apart, and users end up either hand-renaming every session they want to revisit or scrolling through low-signal titles.

This PR auto-generates a short, descriptive title for each new session from the first user/assistant exchange, so session lists stay scannable without manual work. Manual rename continues to work exactly as before and always wins.

Example of a session that used to look like:

help me debug why websocket reconnect loops after deploy on sta...

now shows up as:

Debug websocket reconnect loop after deploy

As a small related change, the rename endpoint now also accepts x-api-key auth so API-driven clients (scripts, the agent API) can apply their own titles through the same endpoint.

Why this is useful

  • Long-running workspaces accumulate many similar-looking sessions; auto-naming makes the sidebar immediately scannable
  • Removes the "rename every useful session manually" friction
  • Keeps manual rename as the source of truth when you do want a custom title
  • Opens the rename endpoint to API-key clients, enabling scripted labelling of sessions created via /api/agent/launch

Guardrails

  • Opt-in: auto-naming only runs when the caller passes autoNameSession: true. The chat WebSocket path and /api/agent/launch opt in; git.js and other non-interactive queryClaudeSDK callers are untouched.
  • Cheap and fire-and-forget: one call to the haiku model per new session, based only on the first exchange, with a 30s timeout. Failures are logged and swallowed — they never affect the chat itself.
  • Manual rename always wins: the module re-checks sessionNamesDb.getName() both before and after the LLM call, so a user rename during the in-flight window is never clobbered.
  • Lightweight update path: instead of triggering a full getProjects() rescan on every auto-naming event, the server emits a small session_name_updated WebSocket message and the frontend patches its local state in place.

Implementation notes

  • New server/session-naming.js generates and persists titles to the existing session_names SQLite table. No schema change.
  • Extracted connectedClients / broadcastProgress / broadcastSessionNameUpdated into server/utils/websocket-clients.js so both the chat WebSocket handler and /api/agent/launch can share them.
  • PUT /api/sessions/:sessionId/rename is registered before the global /api/sessions JWT-only mount so its dual JWT-or-x-api-key handler runs first. Header-only (no query-string API key).

Known limitation

Authorization for the rename endpoint is unchanged from the current upstream behaviour: any authenticated caller who knows a session ID can rename that session. Adding per-user ownership checks would require storing ownership metadata on session_names (schema migration), which felt better scoped as a follow-up PR.

Test plan

  • Start server; a new chat session gets an auto-generated title shortly after the first assistant reply
  • The session label updates in place from a session_name_updated WebSocket event — no full project rescan
  • Manual rename during an in-flight auto-name call is preserved (re-check path)
  • Non-interactive queryClaudeSDK callers (e.g. git.js) do not trigger auto-naming
  • PUT /api/sessions/:id/rename with JWT Authorization header → 200
  • PUT /api/sessions/:id/rename with x-api-key header → 200
  • npx vite build succeeds

Summary by CodeRabbit

  • New Features

    • Sessions are automatically named based on initial conversation messages, creating human-readable session titles
    • Real-time session name updates are instantly reflected across all connected clients and devices
  • Improvements

    • Enhanced authentication options for managing session names
    • Added comprehensive validation for session rename requests, including content length limits

Generate a concise title for new Claude sessions from the first
user/assistant exchange so session lists stay scannable without
manual renames. Auto-naming is opt-in, preserves manual renames,
and runs against a cheap model fire-and-forget.

Also extends PUT /api/sessions/:sessionId/rename to accept either
JWT or x-api-key authentication.
Copilot AI review requested due to automatic review settings April 11, 2026 05:40
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

📝 Walkthrough

Walkthrough

The changes implement automatic session naming for Claude-based conversations. When a conversation completes successfully, the first assistant response is captured and used to generate a human-readable session name via LLM. This name is persisted to the database and broadcast to connected WebSocket clients.

Changes

Cohort / File(s) Summary
Session Naming Core
server/session-naming.js, server/utils/websocket-clients.js
Introduces auto-generation of human-readable session names from first assistant response using Haiku model with 30-second timeout, concurrent guard per sessionId, and WebSocket broadcast utilities for progress and name-update events.
Claude SDK Integration
server/claude-sdk.js, server/index.js, server/routes/agent.js
Captures first assistant message content during SDK streaming loop, passes autoNameSession and broadcastSessionNameUpdated options to queryClaudeSDK, and triggers session name generation on successful completion.
Session Rename API
server/index.js
Adds /api/sessions/:sessionId/rename endpoint with dual authentication (JWT-first, API-key fallback), input validation (sessionId, summary, provider), and persistence via sessionNamesDb.
Client-side Types & State
src/types/app.ts, src/hooks/useProjectsState.ts
Adds SessionNameUpdatedMessage interface and updates useProjectsState hook to receive and apply session name updates from WebSocket broadcasts, syncing changes to both projects and selectedProject state.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Client
    participant Server as Server
    participant Claude as Claude SDK
    participant SessionDB as SessionDB
    participant Broadcast as WebSocket Broadcast
    participant Clients as Connected Clients

    User->>Client: Send chat message
    Client->>Server: POST /api/chat
    Server->>Claude: queryClaudeSDK (with autoNameSession: true)
    Claude->>Claude: Stream response
    Claude-->>Server: First assistant response captured
    Claude-->>Server: Streaming completes
    Server->>SessionDB: Check for existing session name
    alt No existing name
        Server->>Claude: generateSessionName(firstResponse)
        Claude->>Claude: Call Haiku model (30s timeout)
        Claude-->>Server: Generated name
        Server->>SessionDB: Persist auto-generated name
    end
    Server->>Broadcast: broadcastSessionNameUpdated(sessionId, provider, name)
    Broadcast->>Clients: Send session_name_updated message
    Clients->>Client: Receive WebSocket message
    Client->>Client: Update selectedProject.summary
    Client->>User: Display updated session name
Loading

Possibly related PRs

Suggested reviewers

  • blackmammoth
  • viper151

Poem

🐰 A name for each chat, how delightful!
First words from Claude, captured and bright,
Whisked through the wires to friends far and wide,
Sessions remembered, no more mystified!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main features: automatic session naming and API-key authentication support for the rename endpoint.

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

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

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an opt-in “auto-name session” flow that generates a concise session title from the first user/assistant exchange, persists it to the existing session_names table, and updates the UI via a lightweight session_name_updated WebSocket event (avoiding a full projects rescan). It also extends the session rename endpoint to support API-key authentication in addition to JWT.

Changes:

  • Add session_name_updated WebSocket message type and patch sidebar state in-place on receipt.
  • Introduce server/session-naming.js to generate + persist a short session name (Haiku, timeout, manual-rename-safe).
  • Refactor WebSocket client broadcasting into server/utils/websocket-clients.js and enable auto-naming from both chat WS and /api/agent.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/types/app.ts Adds SessionNameUpdatedMessage and includes it in the socket message union.
src/hooks/useProjectsState.ts Handles session_name_updated to update project/session summaries locally without refetching projects.
server/utils/websocket-clients.js New shared connected-client set + broadcast helpers (progress + session name updates).
server/session-naming.js New auto-naming generator + SQLite persistence with in-flight + manual-rename guardrails.
server/routes/agent.js Opts agent-launched Claude sessions into auto-naming and provides broadcast callback.
server/index.js Uses shared WS broadcast utilities; moves/updates rename endpoint to allow JWT-or-API-key auth; opts chat WS sessions into auto-naming.
server/claude-sdk.js Captures first assistant text and fires background auto-name generation for new sessions when opted-in.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +397 to +404
// Fallback: DB API key via x-api-key header only (no query string for security)
const apiKey = req.headers['x-api-key'];
if (apiKey) {
const user = apiKeysDb.validateApiKey(apiKey);
if (user) { req.user = user; return next(); }
return res.status(401).json({ error: 'Invalid or inactive API key' });
}
return res.status(401).json({ error: 'Authentication required (Authorization or x-api-key header)' });
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The rename endpoint’s x-api-key auth conflicts with the global /api validateApiKey middleware (server/middleware/auth.js), which also uses x-api-key for the optional process.env.API_KEY gate. In installs with API_KEY set, requests must send that header value to reach this handler, so there’s no way to also provide the per-user DB API key (and this route intentionally forbids the query-string fallback that /api/agent supports). Consider either (a) using a different header name for per-user keys, (b) changing the global middleware to use a different header, or (c) allowing ?apiKey= only when the global API_KEY gate is enabled so both checks can be satisfied.

Suggested change
// Fallback: DB API key via x-api-key header only (no query string for security)
const apiKey = req.headers['x-api-key'];
if (apiKey) {
const user = apiKeysDb.validateApiKey(apiKey);
if (user) { req.user = user; return next(); }
return res.status(401).json({ error: 'Invalid or inactive API key' });
}
return res.status(401).json({ error: 'Authentication required (Authorization or x-api-key header)' });
// Fallback: DB API key via x-api-key header.
// When the global API_KEY gate is enabled, allow ?apiKey= as an alternate
// transport so clients can satisfy both the global gate and per-user key auth.
const headerApiKey = req.headers['x-api-key'];
const queryApiKey = process.env.API_KEY ? req.query?.apiKey : undefined;
const apiKey = typeof headerApiKey === 'string' && headerApiKey
? headerApiKey
: (typeof queryApiKey === 'string' && queryApiKey ? queryApiKey : undefined);
if (apiKey) {
const user = apiKeysDb.validateApiKey(apiKey);
if (user) { req.user = user; return next(); }
return res.status(401).json({ error: 'Invalid or inactive API key' });
}
const authError = process.env.API_KEY
? 'Authentication required (Authorization, x-api-key header, or apiKey query parameter)'
: 'Authentication required (Authorization or x-api-key header)';
return res.status(401).json({ error: authError });

Copilot uses AI. Check for mistakes.
}
// Fallback: DB API key via x-api-key header only (no query string for security)
const apiKey = req.headers['x-api-key'];
if (apiKey) {
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

req.headers['x-api-key'] can be a string array in Node/Express; passing an array through to apiKeysDb.validateApiKey() will break validation (and could lead to unexpected behavior). Normalize to a single string (or reject arrays) before validating.

Suggested change
if (apiKey) {
if (Array.isArray(apiKey)) {
return res.status(400).json({ error: 'Invalid x-api-key header' });
}
if (typeof apiKey === 'string') {

Copilot uses AI. Check for mistakes.
* @param {string} sessionId
* @param {string} userMessage - First user message
* @param {string|null} assistantResponse - First assistant response
* @param {function|null} broadcastFn - Optional callback to broadcast projects_updated
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The JSDoc for broadcastFn says it broadcasts projects_updated, but the call site now passes broadcastSessionNameUpdated and the code emits a session_name_updated message. Update the comment so it matches the actual event/behavior.

Suggested change
* @param {function|null} broadcastFn - Optional callback to broadcast projects_updated
* @param {function|null} broadcastFn - Optional callback to broadcast session_name_updated

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 `@server/index.js`:
- Around line 422-423: After persisting the rename with
sessionNamesDb.setName(safeSessionId, provider, summary.trim()), also trigger
the live-update path so other clients get the change (emit the
"session_name_updated" event). Call the project/watchers notifier (e.g. the
existing emitter or websocket broadcast function) with the same identifiers and
the trimmed name — include safeSessionId, provider and summary.trim() — before
sending res.json({ success: true }); so JWT/API-key sessions and open clients
receive the update immediately.

In `@src/hooks/useProjectsState.ts`:
- Around line 231-276: The session_name_updated handler always calls setProjects
and may call setSelectedProject/setSelectedSession even when nothing changed,
causing re-renders/loops; update it to compute a new projects array by mapping
projects using the sessionKey helper, track whether any project's sessions were
actually mutated (or the summary changed), and only call setProjects when at
least one project was changed; apply the same no-op guard for selectedProject
and selectedSession (compute updated session arrays, compare/track changes and
only call setSelectedProject/setSelectedSession when something truly changed).
Ensure you reference latestMessage as SessionNameUpdatedMessage, sessionKey,
setProjects, setSelectedProject, setSelectedSession, selectedProject and
selectedSession in the implementation.
🪄 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: fc0fe6a2-5c12-4c36-a762-c6f441c87e4a

📥 Commits

Reviewing files that changed from the base of the PR and between e2459cb and 579ea1a.

📒 Files selected for processing (7)
  • server/claude-sdk.js
  • server/index.js
  • server/routes/agent.js
  • server/session-naming.js
  • server/utils/websocket-clients.js
  • src/hooks/useProjectsState.ts
  • src/types/app.ts

Comment on lines +422 to +423
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
res.json({ success: 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.

⚠️ Potential issue | 🟠 Major

Broadcast manual renames after persisting them.

Line 422 only updates session_names. Those SQLite writes do not hit the project watchers, so JWT/API-key renames never emit session_name_updated and other open clients stay stale until a refresh. The new live-update path needs to be triggered here as well.

💡 Suggested fix
         sessionNamesDb.setName(safeSessionId, provider, summary.trim());
+        broadcastSessionNameUpdated(safeSessionId, provider, summary.trim());
         res.json({ success: 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
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
res.json({ success: true });
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
broadcastSessionNameUpdated(safeSessionId, provider, summary.trim());
res.json({ success: true });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/index.js` around lines 422 - 423, After persisting the rename with
sessionNamesDb.setName(safeSessionId, provider, summary.trim()), also trigger
the live-update path so other clients get the change (emit the
"session_name_updated" event). Call the project/watchers notifier (e.g. the
existing emitter or websocket broadcast function) with the same identifiers and
the trimmed name — include safeSessionId, provider and summary.trim() — before
sending res.json({ success: true }); so JWT/API-key sessions and open clients
receive the update immediately.

Comment on lines +231 to +276
if (latestMessage.type === 'session_name_updated') {
const { sessionId: updatedId, provider, name } = latestMessage as SessionNameUpdatedMessage;

const sessionKey = (p: SessionProvider): keyof Project => {
switch (p) {
case 'cursor': return 'cursorSessions';
case 'codex': return 'codexSessions';
case 'gemini': return 'geminiSessions';
default: return 'sessions';
}
};

setProjects(prev =>
prev.map(project => {
const key = sessionKey(provider);
const sessions = project[key] as ProjectSession[] | undefined;
if (!sessions) return project;

const idx = sessions.findIndex(s => s.id === updatedId);
if (idx === -1) return project;

const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
return { ...project, [key]: updated };
})
);

// Also update selectedProject / selectedSession if they match
if (selectedProject) {
const key = sessionKey(provider);
const sessions = selectedProject[key] as ProjectSession[] | undefined;
if (sessions) {
const idx = sessions.findIndex(s => s.id === updatedId);
if (idx !== -1) {
const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
setSelectedProject({ ...selectedProject, [key]: updated });

if (selectedSession?.id === updatedId) {
setSelectedSession({ ...selectedSession, summary: name });
}
}
}
}

return;
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.

⚠️ Potential issue | 🔴 Critical

Guard session_name_updated against no-op state writes.

Line 243 always produces a new projects array, even when no session matches or the summary is already current. Since this effect depends on projects/selectedProject/selectedSession, the same socket message will retrigger the effect and can spin the UI into an update loop after the first rename event.

💡 Suggested fix
     if (latestMessage.type === 'session_name_updated') {
       const { sessionId: updatedId, provider, name } = latestMessage as SessionNameUpdatedMessage;

       const sessionKey = (p: SessionProvider): keyof Project => {
         switch (p) {
           case 'cursor': return 'cursorSessions';
           case 'codex': return 'codexSessions';
           case 'gemini': return 'geminiSessions';
           default: return 'sessions';
         }
       };

-      setProjects(prev =>
-        prev.map(project => {
-          const key = sessionKey(provider);
-          const sessions = project[key] as ProjectSession[] | undefined;
-          if (!sessions) return project;
-
-          const idx = sessions.findIndex(s => s.id === updatedId);
-          if (idx === -1) return project;
-
-          const updated = [...sessions];
-          updated[idx] = { ...updated[idx], summary: name };
-          return { ...project, [key]: updated };
-        })
-      );
+      const key = sessionKey(provider);
+      setProjects(prev => {
+        let changed = false;
+        const next = prev.map(project => {
+          const sessions = project[key] as ProjectSession[] | undefined;
+          if (!sessions) return project;
+
+          const idx = sessions.findIndex(s => s.id === updatedId);
+          if (idx === -1 || sessions[idx]?.summary === name) return project;
+
+          changed = true;
+          const updated = [...sessions];
+          updated[idx] = { ...updated[idx], summary: name };
+          return { ...project, [key]: updated };
+        });
+
+        return changed ? next : prev;
+      });

-      // Also update selectedProject / selectedSession if they match
-      if (selectedProject) {
-        const key = sessionKey(provider);
-        const sessions = selectedProject[key] as ProjectSession[] | undefined;
-        if (sessions) {
-          const idx = sessions.findIndex(s => s.id === updatedId);
-          if (idx !== -1) {
-            const updated = [...sessions];
-            updated[idx] = { ...updated[idx], summary: name };
-            setSelectedProject({ ...selectedProject, [key]: updated });
-
-            if (selectedSession?.id === updatedId) {
-              setSelectedSession({ ...selectedSession, summary: name });
-            }
-          }
-        }
-      }
+      setSelectedProject(prev => {
+        if (!prev) return prev;
+        const sessions = prev[key] as ProjectSession[] | undefined;
+        if (!sessions) return prev;
+
+        const idx = sessions.findIndex(s => s.id === updatedId);
+        if (idx === -1 || sessions[idx]?.summary === name) return prev;
+
+        const updated = [...sessions];
+        updated[idx] = { ...updated[idx], summary: name };
+        return { ...prev, [key]: updated };
+      });
+
+      setSelectedSession(prev =>
+        prev?.id === updatedId && prev.summary !== name
+          ? { ...prev, summary: name }
+          : prev
+      );

       return;
     }
📝 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 (latestMessage.type === 'session_name_updated') {
const { sessionId: updatedId, provider, name } = latestMessage as SessionNameUpdatedMessage;
const sessionKey = (p: SessionProvider): keyof Project => {
switch (p) {
case 'cursor': return 'cursorSessions';
case 'codex': return 'codexSessions';
case 'gemini': return 'geminiSessions';
default: return 'sessions';
}
};
setProjects(prev =>
prev.map(project => {
const key = sessionKey(provider);
const sessions = project[key] as ProjectSession[] | undefined;
if (!sessions) return project;
const idx = sessions.findIndex(s => s.id === updatedId);
if (idx === -1) return project;
const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
return { ...project, [key]: updated };
})
);
// Also update selectedProject / selectedSession if they match
if (selectedProject) {
const key = sessionKey(provider);
const sessions = selectedProject[key] as ProjectSession[] | undefined;
if (sessions) {
const idx = sessions.findIndex(s => s.id === updatedId);
if (idx !== -1) {
const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
setSelectedProject({ ...selectedProject, [key]: updated });
if (selectedSession?.id === updatedId) {
setSelectedSession({ ...selectedSession, summary: name });
}
}
}
}
return;
if (latestMessage.type === 'session_name_updated') {
const { sessionId: updatedId, provider, name } = latestMessage as SessionNameUpdatedMessage;
const sessionKey = (p: SessionProvider): keyof Project => {
switch (p) {
case 'cursor': return 'cursorSessions';
case 'codex': return 'codexSessions';
case 'gemini': return 'geminiSessions';
default: return 'sessions';
}
};
const key = sessionKey(provider);
setProjects(prev => {
let changed = false;
const next = prev.map(project => {
const sessions = project[key] as ProjectSession[] | undefined;
if (!sessions) return project;
const idx = sessions.findIndex(s => s.id === updatedId);
if (idx === -1 || sessions[idx]?.summary === name) return project;
changed = true;
const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
return { ...project, [key]: updated };
});
return changed ? next : prev;
});
setSelectedProject(prev => {
if (!prev) return prev;
const sessions = prev[key] as ProjectSession[] | undefined;
if (!sessions) return prev;
const idx = sessions.findIndex(s => s.id === updatedId);
if (idx === -1 || sessions[idx]?.summary === name) return prev;
const updated = [...sessions];
updated[idx] = { ...updated[idx], summary: name };
return { ...prev, [key]: updated };
});
setSelectedSession(prev =>
prev?.id === updatedId && prev.summary !== name
? { ...prev, summary: name }
: prev
);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProjectsState.ts` around lines 231 - 276, The
session_name_updated handler always calls setProjects and may call
setSelectedProject/setSelectedSession even when nothing changed, causing
re-renders/loops; update it to compute a new projects array by mapping projects
using the sessionKey helper, track whether any project's sessions were actually
mutated (or the summary changed), and only call setProjects when at least one
project was changed; apply the same no-op guard for selectedProject and
selectedSession (compute updated session arrays, compare/track changes and only
call setSelectedProject/setSelectedSession when something truly changed). Ensure
you reference latestMessage as SessionNameUpdatedMessage, sessionKey,
setProjects, setSelectedProject, setSelectedSession, selectedProject and
selectedSession in the implementation.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +86 to +89
// Re-check: manual rename may have occurred during LLM call
if (sessionNamesDb.getName(sessionId, 'claude')) return;

sessionNamesDb.setName(sessionId, 'claude', name);
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

generateAndPersistSessionName can still clobber a manual rename due to a race between the second getName() check and sessionNamesDb.setName() (which is an unconditional upsert). To make the “manual rename always wins” guarantee true, persist the auto-generated name atomically only if no name exists (e.g., INSERT ... ON CONFLICT DO NOTHING / conditional update), rather than check-then-set in JS.

Suggested change
// Re-check: manual rename may have occurred during LLM call
if (sessionNamesDb.getName(sessionId, 'claude')) return;
sessionNamesDb.setName(sessionId, 'claude', name);
// Persist atomically so a concurrent manual rename always wins.
const didPersist = sessionNamesDb.setNameIfAbsent(sessionId, 'claude', name);
if (!didPersist) return;

Copilot uses AI. Check for mistakes.
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