feat: auto-name new sessions and support API-key renames#643
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCaptures the first assistant response from Claude streams, auto-generates and persists a concise session name when enabled and no custom name exists, and broadcasts a Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client
participant Server as Server
participant Claude as Claude
participant SessionDB as SessionDB
participant Broadcast as Broadcast
participant WSClients as WS Clients
Client->>Server: start chat / create session
Server->>Claude: queryClaudeSDK(..., autoNameSession: true, ...)
Claude-->>Server: stream assistant chunks (capture first assistant text)
Claude-->>Server: stream complete
Server->>SessionDB: getName(sessionId, provider)
alt no custom name
Server->>Claude: generateSessionName(userText, assistantText) (haiku model, timeout)
Claude-->>Server: generated name
Server->>SessionDB: setNameIfAbsent(sessionId, provider, name)
end
Server->>Broadcast: broadcastSessionNameUpdated(sessionId, provider, name)
Broadcast->>WSClients: send session_name_updated
WSClients->>Client: UI/state update
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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_updatedWebSocket message type and patch sidebar state in-place on receipt. - Introduce
server/session-naming.jsto generate + persist a short session name (Haiku, timeout, manual-rename-safe). - Refactor WebSocket client broadcasting into
server/utils/websocket-clients.jsand 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.
| // 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)' }); |
There was a problem hiding this comment.
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.
| // 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 }); |
| } | ||
| // Fallback: DB API key via x-api-key header only (no query string for security) | ||
| const apiKey = req.headers['x-api-key']; | ||
| if (apiKey) { |
There was a problem hiding this comment.
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.
| if (apiKey) { | |
| if (Array.isArray(apiKey)) { | |
| return res.status(400).json({ error: 'Invalid x-api-key header' }); | |
| } | |
| if (typeof apiKey === 'string') { |
| * @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 |
There was a problem hiding this comment.
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.
| * @param {function|null} broadcastFn - Optional callback to broadcast projects_updated | |
| * @param {function|null} broadcastFn - Optional callback to broadcast session_name_updated |
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
server/claude-sdk.jsserver/index.jsserver/routes/agent.jsserver/session-naming.jsserver/utils/websocket-clients.jssrc/hooks/useProjectsState.tssrc/types/app.ts
There was a problem hiding this comment.
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.
| // Re-check: manual rename may have occurred during LLM call | ||
| if (sessionNamesDb.getName(sessionId, 'claude')) return; | ||
|
|
||
| sessionNamesDb.setName(sessionId, 'claude', name); |
There was a problem hiding this comment.
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.
| // 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; |
579ea1a to
d7b11cf
Compare
Rebased & addressed review feedbackRebased onto latest Review feedback addressed
Additional fix (found during post-rebase review)
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 312-328: The new pre-router auth for the rename route blocks
platform-mode requests by returning 401 when no Authorization/x-api-key is
present; modify the handler for app.put('/api/sessions/:sessionId/rename') to
allow IS_PLATFORM deployments to continue to downstream platform/default-user
middleware instead of returning 401—i.e., after checking authHeader and rawKey,
if neither is present and IS_PLATFORM is truthy, call next() (do not
authenticate or return 401) so the existing platform-mode flow
(authenticateToken or platform/default-user middleware) can run; keep existing
behavior for invalid API keys and Bearer tokens.
In `@server/session-naming.js`:
- Around line 87-90: The code logs the generated session title (name) which may
contain sensitive content; change the console.log call in the session-naming
flow (after sessionNamesDb.setNameIfAbsent(sessionId, 'claude', name)) to avoid
printing the name variable — e.g. log only the sessionId or a generic message
like "Session <sessionId> auto-named" — ensuring sessionId,
sessionNamesDb.setNameIfAbsent, and name remain unchanged otherwise and no
sensitive text is written to server logs.
In `@server/utils/websocket-clients.js`:
- Around line 23-31: The broadcaster broadcastSessionNameUpdated currently sends
session name updates to all connectedClients; restrict delivery by attaching the
authenticated user/session context to each WebSocket when added (e.g.,
ws.authUser or ws.sessionId) and change broadcastSessionNameUpdated to accept a
targetSessionId or targetUserId parameter, then filter connectedClients to only
send to clients whose stored context matches that target and whose readyState
=== WebSocket.OPEN; update callers to pass the initiating session/user context
so unrelated authenticated clients are not notified.
🪄 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: 49011a32-ec46-4e72-8ba3-e47092462c83
📒 Files selected for processing (8)
server/claude-sdk.jsserver/database/db.jsserver/index.jsserver/routes/agent.jsserver/session-naming.jsserver/utils/websocket-clients.jssrc/hooks/useProjectsState.tssrc/types/app.ts
✅ Files skipped from review due to trivial changes (1)
- server/claude-sdk.js
🚧 Files skipped from review as they are similar to previous changes (2)
- src/types/app.ts
- src/hooks/useProjectsState.ts
| export function broadcastSessionNameUpdated(sessionId, provider, name) { | ||
| const msg = JSON.stringify({ | ||
| type: 'session_name_updated', | ||
| sessionId, provider, name, | ||
| timestamp: new Date().toISOString() | ||
| }); | ||
| connectedClients.forEach(client => { | ||
| if (client.readyState === WebSocket.OPEN) client.send(msg); | ||
| }); |
There was a problem hiding this comment.
Scope session-name broadcasts to the initiating user/session.
This broadcasts sessionId and name to every connected WebSocket, so one user’s manual/auto-generated session title can be delivered to unrelated authenticated clients. Consider storing the authenticated user on the WebSocket when it is added to connectedClients, then pass a target user/session context into this broadcaster and filter recipients.
🛡️ Possible direction
-export function broadcastSessionNameUpdated(sessionId, provider, name) {
+export function broadcastSessionNameUpdated(sessionId, provider, name, targetUserId = null) {
const msg = JSON.stringify({
type: 'session_name_updated',
sessionId, provider, name,
timestamp: new Date().toISOString()
});
connectedClients.forEach(client => {
- if (client.readyState === WebSocket.OPEN) client.send(msg);
+ if (
+ client.readyState === WebSocket.OPEN &&
+ (!targetUserId || client.userId === targetUserId)
+ ) {
+ client.send(msg);
+ }
});
}Based on learnings, session-scoped authorization gaps in server/index.js are known and should not be expanded by new session-scoped behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/utils/websocket-clients.js` around lines 23 - 31, The broadcaster
broadcastSessionNameUpdated currently sends session name updates to all
connectedClients; restrict delivery by attaching the authenticated user/session
context to each WebSocket when added (e.g., ws.authUser or ws.sessionId) and
change broadcastSessionNameUpdated to accept a targetSessionId or targetUserId
parameter, then filter connectedClients to only send to clients whose stored
context matches that target and whose readyState === WebSocket.OPEN; update
callers to pass the initiating session/user context so unrelated authenticated
clients are not notified.
d7b11cf to
ff23f45
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/hooks/useProjectsState.ts`:
- Around line 273-277: The update to selectedSession only compares prev?.id to
updatedId, allowing cross-provider collisions; modify the logic in the
setSelectedSession call (the setSelectedSession(...) block that references
selectedSession, prev, updatedId, and name) to also compare provider (e.g.,
prev?.provider === updatedProvider) before applying the rename, so only the
session with matching id AND provider is updated; ensure you use the incoming
provider value from the rename event (updatedProvider) and keep the existing
null-safe checks (prev?) so other sessions are unchanged.
🪄 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: 50905869-b2a6-4a47-b02e-74cad06c3e6f
📒 Files selected for processing (8)
server/claude-sdk.jsserver/database/db.jsserver/index.jsserver/routes/agent.jsserver/session-naming.jsserver/utils/websocket-clients.jssrc/hooks/useProjectsState.tssrc/types/app.ts
✅ Files skipped from review due to trivial changes (2)
- server/utils/websocket-clients.js
- server/claude-sdk.js
🚧 Files skipped from review as they are similar to previous changes (1)
- server/index.js
ff23f45 to
210a70d
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/session-naming.js`:
- Around line 43-64: Replace the unreliable q.return?.() timeout cancellation in
the logic that iterates the async iterator returned by query: create an
AbortController, pass its signal into query(...) (the same call site currently
using query({ prompt, options: ... })), replace the external timeout callback to
call controller.abort() after 30s, and still clearTimeout(timeout) in the
finally block; also consider calling timeout.unref() so the timer won't keep the
event loop alive. Keep the model alias 'haiku' as-is and return the sanitized
result via sanitizeSummary as before.
🪄 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: 07b98a14-d555-43f5-bdd9-b2589cec79d5
📒 Files selected for processing (8)
server/claude-sdk.jsserver/database/db.jsserver/index.jsserver/routes/agent.jsserver/session-naming.jsserver/utils/websocket-clients.jssrc/hooks/useProjectsState.tssrc/types/app.ts
✅ Files skipped from review due to trivial changes (1)
- server/utils/websocket-clients.js
🚧 Files skipped from review as they are similar to previous changes (3)
- src/hooks/useProjectsState.ts
- src/types/app.ts
- server/claude-sdk.js
210a70d to
0706a39
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 309-312: The rename session route handler registered as
app.put('/api/sessions/:sessionId/rename') is currently placed after the global
app.use('/api', validateApiKey) middleware and gets blocked when
process.env.API_KEY is set; either move the
app.put('/api/sessions/:sessionId/rename', ...) registration to before the
app.use('/api', validateApiKey) call so the route runs prior to the global
API-key gate, or modify validateApiKey to explicitly skip/allow requests whose
path matches /^\/sessions\/[^/]+\/rename$/ (or check req.path and req.method for
'/api/sessions/:sessionId/rename' and 'PUT') so the route can perform its own DB
API-key validation.
🪄 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: 87516d88-f823-411c-95fa-bd6824a41569
📒 Files selected for processing (8)
server/claude-sdk.jsserver/database/db.jsserver/index.jsserver/routes/agent.jsserver/session-naming.jsserver/utils/websocket-clients.jssrc/hooks/useProjectsState.tssrc/types/app.ts
✅ Files skipped from review due to trivial changes (3)
- server/routes/agent.js
- src/types/app.ts
- server/utils/websocket-clients.js
🚧 Files skipped from review as they are similar to previous changes (2)
- server/claude-sdk.js
- src/hooks/useProjectsState.ts
- Generate concise session titles via Haiku from first user/assistant exchange - Broadcast session_name_updated via WebSocket for real-time UI sync - Extend rename endpoint with dual JWT/API-key authentication - Add setNameIfAbsent for atomic auto-name persistence (no TOCTOU race) - Guard React state updates against no-op re-renders
0706a39 to
ea4b5e9
Compare
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:
now shows up as:
As a small related change, the rename endpoint now also accepts
x-api-keyauth so API-driven clients (scripts, the agent API) can apply their own titles through the same endpoint.Why this is useful
/api/agent/launchGuardrails
autoNameSession: true. The chat WebSocket path and/api/agent/launchopt in;git.jsand other non-interactivequeryClaudeSDKcallers are untouched.haikumodel per new session, based only on the first exchange, with a 30s timeout. Failures are logged and swallowed — they never affect the chat itself.sessionNamesDb.getName()both before and after the LLM call, so a user rename during the in-flight window is never clobbered.getProjects()rescan on every auto-naming event, the server emits a smallsession_name_updatedWebSocket message and the frontend patches its local state in place.Implementation notes
server/session-naming.jsgenerates and persists titles to the existingsession_namesSQLite table. No schema change.connectedClients/broadcastProgress/broadcastSessionNameUpdatedintoserver/utils/websocket-clients.jsso both the chat WebSocket handler and/api/agent/launchcan share them.PUT /api/sessions/:sessionId/renameis registered before the global/api/sessionsJWT-only mount so its dual JWT-or-x-api-keyhandler 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
session_name_updatedWebSocket event — no full project rescanqueryClaudeSDKcallers (e.g.git.js) do not trigger auto-namingPUT /api/sessions/:id/renamewith JWTAuthorizationheader → 200PUT /api/sessions/:id/renamewithx-api-keyheader → 200npx vite buildsucceedsSummary by CodeRabbit
New Features
Improvements