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..6edf1aa9ba 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; + } + + /* 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 + // 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-subtext { + 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; } 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 */ diff --git a/tracking-repos.json b/tracking-repos.json index 9698081a94..9ebcc59de1 100644 --- a/tracking-repos.json +++ b/tracking-repos.json @@ -1,5 +1,5 @@ { "phoenixPro": { - "commitID": "58084dc226fc826ea6b93ee6ef8f8b36c5f1b193" + "commitID": "fdbb193c654554398abb77e5082a7348fc728f15" } }