Skip to content

feat(conversation): split title into customTitle/engineTitle with render-time resolution#125

Merged
realDuang merged 11 commits intomainfrom
codemux/feat-conv-title-summary
Apr 29, 2026
Merged

feat(conversation): split title into customTitle/engineTitle with render-time resolution#125
realDuang merged 11 commits intomainfrom
codemux/feat-conv-title-summary

Conversation

@realDuang
Copy link
Copy Markdown
Owner

@realDuang realDuang commented Apr 22, 2026

Closes #126

Summary

  • Split conversation titles into customTitle, engineTitle, and firstPrompt, with display resolved as customTitle ?? usableEngineTitle ?? firstPrompt ?? "New Chat".
  • Persist engine titles only when adapters observe meaningful engine-provided titles; filter OpenCode default placeholders, Copilot first-prompt fallback summaries, and custom-title echoes.
  • Remove the unreliable Claude SDK summary polling path; keep engine-side rename writeback where supported and refresh OpenCode/Copilot titles from their own session metadata/events.

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.ts
  • bun run typecheck
  • bun run test:unit (65 files / 2307 tests)
  • OpenCode probe: with small_model: "opencode/gpt-5-nano", OpenCode generated a non-default title and CodeMux adapter/EngineManager observed it into engineTitle.

🤖 Generated with Claude Code

…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>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 75.17% (🎯 50%) 10743 / 14290
🔵 Statements 73.78% (🎯 50%) 11431 / 15493
🔵 Functions 71.09% (🎯 50%) 1798 / 2529
🔵 Branches 68.29% (🎯 50%) 6669 / 9765
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
electron/main/engines/engine-adapter.ts 94.44% 68.75% 78.57% 93.33% 310
electron/main/engines/claude/index.ts 65.68% 57.6% 57.97% 66.72% 165-176, 193-194, 194, 281-468, 476, 531-544, 593-611, 618-619, 827-831, 877-881, 920, 924-925, 933-936, 974, 982-984, 990-1210, 1262-1265, 1375-1474, 1503-1505, 1581-1672, 1711-1727, 1752, 1768, 1769, 1800-1801, 1803-1828, 1834-1858, 1878, 1894-1904, 1909, 1912, 1918, 1922, 1973, 2014, 2030, 2131, 2135-2146, 2198-2220, 2233, 2257-2291, 2303-2305, 2369, 2394-2399, 2448, 2473-2523, 2903, 2986, 3046, 3332, 3388-3389, 3449-3450, 3454-3455, 3492-3556
electron/main/engines/codex/index.ts 78.5% 61.83% 87.66% 83.96% 85-99, 229, 230, 245, 251-254, 273, 342, 380, 390, 437, 458, 467-480, 499-500, 523, 544, 547, 566, 587, 598, 601, 604, 614-615, 624, 661, 666, 676-679, 692-700, 712, 717, 739, 748-755, 771, 797, 808, 834-884, 898-899, 904, 921, 956-957, 980, 1025, 1039, 1049, 1060, 1123-1124, 1132-1161, 1180, 1183, 1198, 1200, 1217, 1229, 1235, 1243, 1255-1256, 1259-1260, 1277, 1278, 1328, 1334, 1417, 1425, 1433, 1441, 1443, 1453, 1455, 1464, 1466, 1475, 1483, 1490, 1498, 1633, 1686-1687, 1708, 1728-1729, 1766, 1772, 1788-1789, 1815, 1825, 1839, 1875, 1879, 1909, 1969, 1974, 2042, 2049, 2057, 2065-2078, 2083, 2088, 2183, 2222, 2224, 2229, 2235, 2239, 2241, 2243, 2250, 2265, 2295, 2304, 2312-2339, 2344, 2351, 2356, 2358, 2361, 2380-2388, 2392
electron/main/engines/copilot/index.ts 74.72% 67.74% 70.66% 75.83% 125, 152-154, 160, 165-168, 182, 183, 308, 366-377, 390-391, 419, 428, 436, 469, 543, 550, 553, 584-588, 594-603, 638-649, 653-662, 706-770, 778-783, 848-881, 906, 927-933, 941, 951, 968-974, 1080, 1087, 1110-1111, 1132-1133, 1148, 1149, 1158, 1162, 1165-1167, 1176-1196, 1221-1226, 1300-1302, 1320-1321, 1391, 1461, 1468, 1529, 1548-1569, 1583, 1634-1645, 1671-1702, 1714, 1717, 1718, 1720, 1838
electron/main/engines/opencode/converters.ts 97.29% 83.54% 85.71% 97.22% 64-68
electron/main/engines/opencode/index.ts 81.07% 66.83% 75.94% 82.89% 113, 183-192, 197-199, 210-211, 282, 486, 561-569, 632, 635, 641, 756, 775, 877, 883-884, 892, 905-917, 936-947, 1085, 1106-1122, 1146, 1155, 1178-1208, 1323-1343, 1388
electron/main/gateway/engine-manager.ts 96.94% 85.67% 97.77% 97.92% 51, 52, 58, 202-206, 372-381, 460, 470, 570, 589, 670, 927, 936-940, 1338
electron/main/services/conversation-store.ts 75.92% 63.51% 77.41% 77.81% 206-209, 311-337, 395, 512-616, 702-703
src/lib/session-utils.ts 95.65% 95.45% 100% 100% 20
Generated in workflow #439 for commit 4989d7e by the Vitest Coverage Report Action

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 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.title into optional legacy title plus customTitle, engineTitle, and firstPrompt, and derive UnifiedSession.title via priority resolution at conversion time.
  • Replace rename()/title-fallback interception with setCustomTitle() + setEngineTitle(), and add adapter-level renameSession() 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 the engineSessionId from 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, firstPrompt is captured inside ConversationStore.appendMessage, but no session.updated is emitted when the first user message is persisted. The frontend sidebar title is only updated via session.updated events, so new conversations may stay titled "New Chat" until an engine pushes a title. Consider emitting a session.updated after persistUserMessage() when it sets firstPrompt for 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.

Comment thread electron/main/engines/opencode/index.ts Outdated
Comment thread electron/main/engines/claude/index.ts Outdated
realDuang and others added 10 commits April 22, 2026 17:24
- 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>
@realDuang realDuang merged commit 299d447 into main Apr 29, 2026
11 checks passed
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.

Conversation titles don't auto-summarize and rename doesn't sync to engine

2 participants