From 89ca2987092e9933def1be0794e232e773c13d04 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 8 Sep 2026 12:35:49 +0530 Subject: [PATCH 1/4] feat(ai): use Claude's own session titles in chat history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CLI writes an `ai-title` entry into every session transcript — a short summary of what the conversation is about — and the panel was throwing it away in favour of the first user message truncated at 80 chars. Read it back through the SDK's getSessionInfo() and hand it to the browser on aiComplete, plus a getSessionTitles peer call so the history list can upgrade entries recorded before this. getSessionInfo() falls back to the raw first prompt when a session has no AI title, and ours is the enriched prompt (it opens with the editor context line), so only a title that beats that fallback is reported. Also fixes two history panel bugs found alongside: - The dropdown is a sibling of .ai-chat-body-chat, not a child as the LESS comment claimed, so hiding that wrapper's children left an empty flex:1 box claiming half the panel and the list stopped halfway down. Hide the wrapper itself. - .ai-session-history-dropdown was in the issue#10150 scrollbar workaround's selector list, which sets background-color:inherit on hover. The dropdown has a tint of its own, so the whole list flashed lighter under the pointer. --- src-node/claude-code-agent.js | 77 +++++++++++++++++++++++++++-- src/styles/Extn-AIChatPanel.less | 46 ++++++++++++++--- src/styles/brackets_scrollbars.less | 6 ++- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/src-node/claude-code-agent.js b/src-node/claude-code-agent.js index 9c70c91b81..cdceb87ed1 100644 --- a/src-node/claude-code-agent.js +++ b/src-node/claude-code-agent.js @@ -562,6 +562,45 @@ async function getQueryFn() { return queryModule.query; } +/** + * Best-effort AI-generated title for a session. + * + * The CLI writes an `ai-title` entry into the session JSONL on the first + * turn — a few words describing what the conversation is actually about. + * The SDK surfaces it as `SDKSessionInfo.summary`, preferring a user-set + * custom title and falling back to the raw first prompt when neither + * exists. We only report a title that beats that fallback, so the panel + * keeps its own first-message title when the CLI has nothing better + * (older CLI builds, or a session that ended before the title landed). + * + * @param {string} sessionId + * @param {string} projectPath + * @return {Promise} short title, or null when unavailable + */ +async function _getAISessionTitle(sessionId, projectPath) { + if (!sessionId) { + return null; + } + try { + if (!queryModule) { + queryModule = await import("@anthropic-ai/claude-agent-sdk"); + } + if (typeof queryModule.getSessionInfo !== "function") { + return null; + } + const info = await queryModule.getSessionInfo(sessionId, + projectPath ? { dir: projectPath } : undefined); + const title = info && info.summary ? info.summary.trim() : ""; + if (!title || title === (info.firstPrompt || "").trim()) { + return null; + } + return title; + } catch (e) { + console.log("[Phoenix AI] Session title lookup failed:", e.message); + return null; + } +} + /** * Build ordered candidate paths on Windows, split into two tiers: * - `native`: real PE binaries dropped by claude.ai/install.ps1 or the @@ -1049,6 +1088,35 @@ exports.resumeSession = async function (params) { /** * Destroy the current session (clear session ID). */ +/** + * AI titles for sessions already recorded in the panel's history. + * + * Lets the history list upgrade entries whose stored title is still the + * first user message truncated mid-sentence — the CLI's own title has + * been sitting in those transcripts all along. Sessions with nothing + * better than the raw first prompt are simply left out of the result. + * + * @param {{projectPath: string, sessionIds: Array}} params + * @return {Promise} map of sessionId -> title + */ +exports.getSessionTitles = async function (params) { + const { projectPath, sessionIds } = params || {}; + const titles = {}; + if (!Array.isArray(sessionIds)) { + return titles; + } + // Sequential: each lookup parses one session transcript, and these can + // be large. A history list is at most a few dozen entries, and this + // runs behind an already-rendered dropdown, so latency is not critical. + for (const sessionId of sessionIds) { + const title = await _getAISessionTitle(sessionId, projectPath); + if (title) { + titles[sessionId] = title; + } + } + return titles; +}; + exports.destroySession = async function () { if (currentAbortController) { currentAbortController.abort(); @@ -2758,7 +2826,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // Signal completion nodeConnector.triggerPeer("aiComplete", { requestId: requestId, - sessionId: currentSessionId + sessionId: currentSessionId, + sessionTitle: await _getAISessionTitle(currentSessionId, projectPath) }); } catch (err) { @@ -2772,7 +2841,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // session — the abort just leaves an interrupt marker in the log. nodeConnector.triggerPeer("aiComplete", { requestId: requestId, - sessionId: currentSessionId + sessionId: currentSessionId, + sessionTitle: await _getAISessionTitle(currentSessionId, projectPath) }); return; } @@ -2828,7 +2898,8 @@ async function _runQuery(requestId, prompt, projectPath, model, signal, locale, // Always send aiComplete after aiError so the UI exits streaming state nodeConnector.triggerPeer("aiComplete", { requestId: requestId, - sessionId: currentSessionId + sessionId: currentSessionId, + sessionTitle: await _getAISessionTitle(currentSessionId, projectPath) }); } } diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 0fe0e53d9a..363f611c53 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -517,13 +517,12 @@ /* ── Session history dropdown ──────────────────────────────────────── */ /* When history is open, hide chat content and show the dropdown instead. - These live inside .ai-chat-body-chat now (not direct .ai-chat-panel - children), since that wrapper is what CLI mode toggles alongside it. */ + The dropdown is a sibling of .ai-chat-body-chat (the wrapper CLI mode + toggles), and both are flex:1 — so the wrapper has to go out of the + layout entirely, not just have its children hidden, or it keeps + claiming half the panel height and the list stops halfway down. */ .ai-chat-panel.ai-history-open { - .ai-chat-body-chat > .ai-chat-messages, - .ai-chat-body-chat > .ai-chat-status, - .ai-chat-body-chat > .ai-chat-input-area, - .ai-chat-body-chat > .ai-onboarding-wrap { + > .ai-chat-body-chat { display: none !important; } } @@ -574,10 +573,45 @@ text-overflow: ellipsis; } + .ai-history-item-meta { + display: flex; + align-items: baseline; + gap: 5px; + min-width: 0; + } + .ai-history-item-time { font-size: @ai-text-meta; color: @project-panel-text-2; opacity: 0.6; + flex-shrink: 0; + } + + /* Last prompt of the session, revealed on hover: always-on it made a + 50-entry list too dense to scan. It keeps its space in the row while + hidden, so fading it in shifts nothing. Empty when it would only + repeat the title, and the separator hangs off the text so nothing + is drawn for those rows. */ + .ai-history-item-preview { + font-size: @ai-text-meta; + color: @project-panel-text-2; + // No transition: a fade here reads as lag when you are running the + // pointer down the list looking for a session. + opacity: 0; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:not(:empty)::before { + content: "·"; + margin-right: 5px; + } + } + + &:hover .ai-history-item-preview { + opacity: 0.75; } .ai-history-item-delete { diff --git a/src/styles/brackets_scrollbars.less b/src/styles/brackets_scrollbars.less index 602a27f474..71595bec98 100644 --- a/src/styles/brackets_scrollbars.less +++ b/src/styles/brackets_scrollbars.less @@ -22,10 +22,12 @@ /* Fix for issue#10150 for weird project tree scrollbar behaviour. Scrollbar not appearing on hover. Forcing the div to render displays the scrollbar. So setting transparent background color on hover. */ +/* .ai-session-history-dropdown is deliberately not in this list: it has a + background tint of its own, and inheriting the panel's on hover made the + whole list flash lighter under the pointer. */ .open-files-container:hover, #project-files-container:hover, -.ai-chat-messages:hover, -.ai-session-history-dropdown:hover { +.ai-chat-messages:hover { background-color: inherit; } From cf76adb8ff94d93c4ddef0eec335cf2bf2bc3d16 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 8 Sep 2026 12:39:27 +0530 Subject: [PATCH 2/4] build: update pro deps --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index 9698081a94..eea51088c9 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "58084dc226fc826ea6b93ee6ef8f8b36c5f1b193" + "commitID": "b4dc14d92b13ec7376144236f8596a877dc824a7" } } From 27e3b8c61e3279f07243cfb915b68b5ec7a6af28 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 8 Sep 2026 12:52:30 +0530 Subject: [PATCH 3/4] feat: widen the default sidebar to 320px Also renames .ai-history-item-preview to .ai-history-item-subtext: the span holds Claude's session title now, not a preview of the chat. --- src/styles/Extn-AIChatPanel.less | 14 +++++++------- src/styles/brackets_variables.less | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/styles/Extn-AIChatPanel.less b/src/styles/Extn-AIChatPanel.less index 363f611c53..6edf1aa9ba 100644 --- a/src/styles/Extn-AIChatPanel.less +++ b/src/styles/Extn-AIChatPanel.less @@ -587,12 +587,12 @@ flex-shrink: 0; } - /* Last prompt of the session, revealed on hover: always-on it made a - 50-entry list too dense to scan. It keeps its space in the row while - hidden, so fading it in shifts nothing. Empty when it would only - repeat the title, and the separator hangs off the text so nothing - is drawn for those rows. */ - .ai-history-item-preview { + /* Claude's title for the session, revealed on hover: always-on it made + a 50-entry list too dense to scan. It keeps its space in the row while + hidden, so showing it shifts nothing. Empty when it would only repeat + the headline, and the separator hangs off the text so nothing is + drawn for those rows. */ + .ai-history-item-subtext { font-size: @ai-text-meta; color: @project-panel-text-2; // No transition: a fade here reads as lag when you are running the @@ -610,7 +610,7 @@ } } - &:hover .ai-history-item-preview { + &:hover .ai-history-item-subtext { opacity: 0.75; } diff --git a/src/styles/brackets_variables.less b/src/styles/brackets_variables.less index 1a3ca9915b..27bca2aa5c 100644 --- a/src/styles/brackets_variables.less +++ b/src/styles/brackets_variables.less @@ -48,7 +48,7 @@ @inline-triangle-size: 9px; /* Main layout */ -@sidebar-width: 270px; // user resizable, however +@sidebar-width: 320px; // user resizable, however @main-toolbar-width: 30px; /* Extension Manager */ From 7a2b72933de7628f787f2f1251423f1cfb596570 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 8 Sep 2026 12:54:24 +0530 Subject: [PATCH 4/4] build: update pro deps --- tracking-repos.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracking-repos.json b/tracking-repos.json index eea51088c9..9ebcc59de1 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "b4dc14d92b13ec7376144236f8596a877dc824a7" + "commitID": "fdbb193c654554398abb77e5082a7348fc728f15" } }