Conversation
…der-time resolution
Replace single-title write path with priority-based fields persisted on
ConversationMeta:
- customTitle: user-set via rename
- engineTitle: pushed from engine session.updated / Claude aiTitle
- firstPrompt: captured from first user message as last-resort fallback
displayTitle is computed at render time as
customTitle ?? engineTitle ?? title (legacy) ?? firstPrompt ?? "New Chat".
Engine writeback on user rename:
- Claude: renameSession via @anthropic-ai/claude-agent-sdk
- Codex: thread/name/set JSON-RPC
- OpenCode: client.session.update({ title })
- Copilot: local-only (public SDK lacks the API)
Claude engine refreshes engineTitle by polling getSessionInfo() with a
1s debounce after each result event, surfacing the SDK's pre-computed
summary (custom title -> aiTitle -> first prompt).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors conversation titling to separate user-set vs engine-provided titles, and derives the displayed session title at conversion/render time to simplify title fallback logic and enable best-effort engine-side rename writeback.
Changes:
- Split
ConversationMeta.titleinto optional legacytitlepluscustomTitle,engineTitle, andfirstPrompt, and deriveUnifiedSession.titlevia priority resolution at conversion time. - Replace
rename()/title-fallback interception withsetCustomTitle()+setEngineTitle(), and add adapter-levelrenameSession()hooks for Claude/Codex/OpenCode writeback. - Add Claude-side debounced
getSessionInfo()refresh to emit engineTitle updates; update unit tests accordingly.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/types/unified.ts |
Makes title legacy/optional and adds customTitle/engineTitle/firstPrompt fields. |
electron/main/services/conversation-store.ts |
Removes store-level title initialization; adds setters for custom/engine title and captures firstPrompt from first user message (and imports). |
electron/main/gateway/engine-manager.ts |
Derives UnifiedSession.title via priority resolution; updates session.updated handling; implements rename writeback to adapters; removes applyTitleFallback logic. |
electron/main/engines/engine-adapter.ts |
Adds default no-op renameSession() API for engine-side rename support. |
electron/main/engines/opencode/index.ts |
Implements OpenCode renameSession() via session.update({ title }). |
electron/main/engines/codex/index.ts |
Implements Codex renameSession() via thread/name/set RPC and updates local thread cache. |
electron/main/engines/claude/index.ts |
Implements Claude renameSession() via SDK and adds debounced getSessionInfo()-based engineTitle refresh after each turn. |
tests/unit/electron/services/conversation-store.test.ts |
Updates expectations for removed auto-title behavior; adds coverage for firstPrompt and customTitle setters. |
tests/unit/electron/gateway/engine-manager.test.ts |
Updates mocks/assertions for setCustomTitle/setEngineTitle and removes applyTitleFallback tests. |
Comments suppressed due to low confidence (2)
electron/main/gateway/engine-manager.ts:334
- In the session.updated handler, setEngineSession is called with
conversationStore.get(convId)?.engineSessionId ?? "". If the mapping was resolved from the in-memory cache before engineSessionId was persisted, this will overwrite the stored engineSessionId with an empty string, breaking future resolveConversationId/findByEngineSession lookups. Use theengineSessionIdfrom the event (the adapter-emitted engine session id) instead of falling back to "".
if (data.session.engineMeta) {
conversationStore.setEngineSession(
convId,
conversationStore.get(convId)?.engineSessionId ?? "",
data.session.engineMeta as Record<string, unknown>,
);
electron/main/gateway/engine-manager.ts:883
- After removing applyTitleFallback,
firstPromptis captured inside ConversationStore.appendMessage, but nosession.updatedis emitted when the first user message is persisted. The frontend sidebar title is only updated viasession.updatedevents, so new conversations may stay titled "New Chat" until an engine pushes a title. Consider emitting asession.updatedafterpersistUserMessage()when it setsfirstPromptfor the first time (without overwriting customTitle/engineTitle), so the derived display title updates immediately.
// Persist user message before sending to engine
// (Some adapters like OpenCode don't emit user message events)
await this.persistUserMessage(sessionId, content);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- opencode renameSession: use openCodeLog.warn instead of console.warn - claude: clear pendingTitleRefreshes timers in deleteSession/stop and unref the timer so it doesn't keep the event loop alive at shutdown Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CC's getSessionInfo().summary can return garbage when the session contains tool/system-injected user messages (e.g. <task-notification> blocks from background agents — the SDK feeds them into the aiTitle generator and the resulting summary is the literal XML). Detect strings that start with an XML/HTML-style opening tag and fall back to firstPrompt for the engineTitle write-back. This also self-heals previously persisted junk titles on the next turn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the unsafe Claude summary polling path and keep title resolution limited to adapter-observed engine updates, user custom titles, and first-prompt fallback. Filter default/prompt-derived titles conservatively so real engine summaries can replace firstPrompt without being overwritten by placeholders or prompt echoes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Claude title polling path was removed because SDK summaries are not a reliable engine title source. Drop the tests for the deleted XML-style title filter so the suite matches the supported title lifecycle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the leftover blank-line-only change from the deleted Claude title filter tests so the PR diff only includes relevant files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolve Copilot permission handling conflicts by keeping the updated session-scoped approval flow from main while preserving title refresh behavior. Update OpenCode provider conversion for the current SDK schema. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Handle both OpenCode provider model shapes when converting costs and capabilities so typecheck passes with the SDK version installed in CI and local development. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolve session config conflicts from the latest main while preserving resolved conversation title display. Keep first-prompt title updates alongside queued message timing and align conversation store tests with customTitle metadata. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace real prompts and generated titles in title-related unit tests with neutral mock fixture text so test coverage does not expose user session content. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #126
Summary
customTitle,engineTitle, andfirstPrompt, with display resolved ascustomTitle ?? usableEngineTitle ?? firstPrompt ?? "New Chat".Test plan
bunx vitest run tests/unit/src/lib/session-utils.test.ts tests/unit/electron/gateway/engine-manager.test.ts tests/unit/electron/engines/copilot/index.test.ts tests/unit/electron/engines/opencode/converters.test.ts tests/unit/electron/services/conversation-store.test.tsbun run typecheckbun run test:unit(65 files / 2307 tests)small_model: "opencode/gpt-5-nano", OpenCode generated a non-default title and CodeMux adapter/EngineManager observed it intoengineTitle.🤖 Generated with Claude Code