Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions src-node/claude-code-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|null>} 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
Expand Down Expand Up @@ -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<string>}} params
* @return {Promise<Object>} 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();
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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)
});
}
}
46 changes: 40 additions & 6 deletions src/styles/Extn-AIChatPanel.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions src/styles/brackets_scrollbars.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/styles/brackets_variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion tracking-repos.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"phoenixPro": {
"commitID": "58084dc226fc826ea6b93ee6ef8f8b36c5f1b193"
"commitID": "fdbb193c654554398abb77e5082a7348fc728f15"
}
}
Loading