feat: group sidebar sessions by project
This commit is contained in:
175
public/app.js
175
public/app.js
@@ -1288,6 +1288,109 @@
|
|||||||
return sessions.filter((s) => normalizeAgent(s.agent) === currentAgent);
|
return sessions.filter((s) => normalizeAgent(s.agent) === currentAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSessionCwdFromCache(sessionId) {
|
||||||
|
if (!sessionId) return '';
|
||||||
|
const cachedCwd = sessionCache.get(sessionId)?.snapshot?.cwd;
|
||||||
|
if (cachedCwd) return cachedCwd;
|
||||||
|
if (sessionId === currentSessionId && currentCwd) return currentCwd;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionEffectiveCwd(session) {
|
||||||
|
return session?.cwd || getSessionCwdFromCache(session?.id) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionProjectName(session) {
|
||||||
|
if (session?.projectName) return session.projectName;
|
||||||
|
const cwd = String(getSessionEffectiveCwd(session)).replace(/\\/g, '/').replace(/\/+$/, '');
|
||||||
|
return cwd ? (getPathLeaf(cwd) || cwd) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupSessionsByProject(sessionItems) {
|
||||||
|
const groups = [];
|
||||||
|
const groupMap = new Map();
|
||||||
|
const ungroupedSessions = [];
|
||||||
|
for (const session of sessionItems) {
|
||||||
|
const projectName = getSessionProjectName(session);
|
||||||
|
if (!projectName) {
|
||||||
|
ungroupedSessions.push(session);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!groupMap.has(projectName)) {
|
||||||
|
const cwd = getSessionEffectiveCwd(session);
|
||||||
|
const group = {
|
||||||
|
name: projectName,
|
||||||
|
cwd,
|
||||||
|
sessions: [],
|
||||||
|
latestUpdated: session.updated || '',
|
||||||
|
};
|
||||||
|
groupMap.set(projectName, group);
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
const group = groupMap.get(projectName);
|
||||||
|
group.sessions.push(session);
|
||||||
|
if (new Date(session.updated || 0) > new Date(group.latestUpdated || 0)) {
|
||||||
|
group.latestUpdated = session.updated || group.latestUpdated;
|
||||||
|
group.cwd = getSessionEffectiveCwd(session) || group.cwd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
groups: groups.sort((a, b) => new Date(b.latestUpdated || 0) - new Date(a.latestUpdated || 0)),
|
||||||
|
ungroupedSessions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSessionListItem(session) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = `session-item${session.id === currentSessionId ? ' active' : ''}`;
|
||||||
|
item.dataset.id = session.id;
|
||||||
|
const sessionCwd = getSessionEffectiveCwd(session);
|
||||||
|
if (sessionCwd) item.title = sessionCwd;
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="session-item-main">
|
||||||
|
<span class="session-item-title">${escapeHtml(session.title || 'Untitled')}</span>
|
||||||
|
${session.isRunning ? '<span class="session-item-status">运行中</span>' : ''}
|
||||||
|
</div>
|
||||||
|
${session.hasUnread ? '<span class="session-unread-dot"></span>' : ''}
|
||||||
|
<span class="session-item-time">${timeAgo(session.updated)}</span>
|
||||||
|
<div class="session-item-actions">
|
||||||
|
<button class="session-item-btn edit" title="重命名">✎</button>
|
||||||
|
<button class="session-item-btn delete" title="删除">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
const target = e.target;
|
||||||
|
if (target.classList.contains('delete')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const doDelete = () => {
|
||||||
|
if (getLastSessionForAgent(currentAgent) === session.id) {
|
||||||
|
localStorage.removeItem(getAgentSessionStorageKey(currentAgent));
|
||||||
|
}
|
||||||
|
invalidateSessionCache(session.id);
|
||||||
|
send({ type: 'delete_session', sessionId: session.id });
|
||||||
|
if (session.id === currentSessionId) {
|
||||||
|
resetChatView(currentAgent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (skipDeleteConfirm) {
|
||||||
|
doDelete();
|
||||||
|
} else {
|
||||||
|
showDeleteConfirm(session.agent, doDelete);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target.classList.contains('edit')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
startEditSessionTitle(item, session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openSession(session.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
function updateCwdBadge() {
|
function updateCwdBadge() {
|
||||||
if (!chatCwd) return;
|
if (!chatCwd) return;
|
||||||
if (currentCwd) {
|
if (currentCwd) {
|
||||||
@@ -1754,6 +1857,16 @@
|
|||||||
case 'session_info':
|
case 'session_info':
|
||||||
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
|
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
|
||||||
const snapshot = normalizeSessionSnapshot(msg);
|
const snapshot = normalizeSessionSnapshot(msg);
|
||||||
|
sessions = sessions.map((session) => (
|
||||||
|
session.id === snapshot.sessionId
|
||||||
|
? {
|
||||||
|
...session,
|
||||||
|
cwd: snapshot.cwd || session.cwd || '',
|
||||||
|
projectName: snapshot.cwd ? getPathLeaf(snapshot.cwd) : session.projectName || '',
|
||||||
|
title: snapshot.title || session.title,
|
||||||
|
}
|
||||||
|
: session
|
||||||
|
));
|
||||||
if (activeSessionLoad?.sessionId === msg.sessionId) {
|
if (activeSessionLoad?.sessionId === msg.sessionId) {
|
||||||
activeSessionLoad.snapshot = snapshot;
|
activeSessionLoad.snapshot = snapshot;
|
||||||
}
|
}
|
||||||
@@ -3135,53 +3248,29 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const s of visibleSessions) {
|
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(visibleSessions);
|
||||||
const item = document.createElement('div');
|
for (const group of projectGroups) {
|
||||||
item.className = `session-item${s.id === currentSessionId ? ' active' : ''}`;
|
const groupEl = document.createElement('section');
|
||||||
item.dataset.id = s.id;
|
groupEl.className = 'session-project-group';
|
||||||
item.innerHTML = `
|
|
||||||
<div class="session-item-main">
|
const header = document.createElement('div');
|
||||||
<span class="session-item-title">${escapeHtml(s.title || 'Untitled')}</span>
|
header.className = 'session-project-header';
|
||||||
${s.isRunning ? '<span class="session-item-status">运行中</span>' : ''}
|
header.title = group.cwd || group.name;
|
||||||
</div>
|
header.innerHTML = `
|
||||||
${s.hasUnread ? '<span class="session-unread-dot"></span>' : ''}
|
<span class="session-project-name">${escapeHtml(group.name)}</span>
|
||||||
<span class="session-item-time">${timeAgo(s.updated)}</span>
|
<span class="session-project-count">${group.sessions.length}</span>
|
||||||
<div class="session-item-actions">
|
|
||||||
<button class="session-item-btn edit" title="重命名">✎</button>
|
|
||||||
<button class="session-item-btn delete" title="删除">×</button>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
groupEl.appendChild(header);
|
||||||
|
|
||||||
item.addEventListener('click', (e) => {
|
for (const s of group.sessions) {
|
||||||
const target = e.target;
|
groupEl.appendChild(createSessionListItem(s));
|
||||||
if (target.classList.contains('delete')) {
|
|
||||||
e.stopPropagation();
|
|
||||||
const doDelete = () => {
|
|
||||||
if (getLastSessionForAgent(currentAgent) === s.id) {
|
|
||||||
localStorage.removeItem(getAgentSessionStorageKey(currentAgent));
|
|
||||||
}
|
}
|
||||||
invalidateSessionCache(s.id);
|
|
||||||
send({ type: 'delete_session', sessionId: s.id });
|
|
||||||
if (s.id === currentSessionId) {
|
|
||||||
resetChatView(currentAgent);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (skipDeleteConfirm) {
|
|
||||||
doDelete();
|
|
||||||
} else {
|
|
||||||
showDeleteConfirm(s.agent, doDelete);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (target.classList.contains('edit')) {
|
|
||||||
e.stopPropagation();
|
|
||||||
startEditSessionTitle(item, s);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openSession(s.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
sessionList.appendChild(item);
|
sessionList.appendChild(groupEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const s of ungroupedSessions) {
|
||||||
|
sessionList.appendChild(createSessionListItem(s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,17 @@ html[data-theme='coolvibe'] .attachment-tray-note {
|
|||||||
border-color: rgba(191, 220, 228, 0.96);
|
border-color: rgba(191, 220, 228, 0.96);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='coolvibe'] .session-project-header {
|
||||||
|
background: linear-gradient(180deg, rgba(247, 251, 252, 0.94), rgba(239, 248, 250, 0.88));
|
||||||
|
color: #5f7f87;
|
||||||
|
border-bottom: 1px solid rgba(191, 220, 228, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='coolvibe'] .session-project-count {
|
||||||
|
background: rgba(8, 145, 178, 0.1);
|
||||||
|
color: #0a6d83;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme='coolvibe'] .session-item {
|
html[data-theme='coolvibe'] .session-item {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -290,6 +301,17 @@ html[data-theme='coolvibe'] .theme-card.active {
|
|||||||
box-shadow: 0 0 0 2px rgba(8, 145, 178, 0.14);
|
box-shadow: 0 0 0 2px rgba(8, 145, 178, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='editorial'] .session-project-header {
|
||||||
|
background: linear-gradient(180deg, rgba(239, 232, 220, 0.94), rgba(246, 241, 232, 0.84));
|
||||||
|
color: #7f6f61;
|
||||||
|
border-bottom: 1px solid rgba(139, 94, 60, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='editorial'] .session-project-count {
|
||||||
|
background: rgba(139, 94, 60, 0.1);
|
||||||
|
color: #71482d;
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme='editorial'] {
|
html[data-theme='editorial'] {
|
||||||
--bg-primary: #f6f1e8;
|
--bg-primary: #f6f1e8;
|
||||||
--bg-secondary: #efe8dc;
|
--bg-secondary: #efe8dc;
|
||||||
@@ -621,6 +643,46 @@ body.session-loading-active {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
background: rgba(255, 249, 242, 0.7);
|
background: rgba(255, 249, 242, 0.7);
|
||||||
}
|
}
|
||||||
|
.session-project-group {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.session-project-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 4px 2px 5px;
|
||||||
|
padding: 6px 8px 5px;
|
||||||
|
background: rgba(242, 235, 226, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.session-project-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.session-project-count {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
.session-item {
|
.session-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1460,12 +1460,15 @@ function sendSessionList(ws) {
|
|||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
try {
|
try {
|
||||||
const s = normalizeSession(JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8')));
|
const s = normalizeSession(JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8')));
|
||||||
|
const cwd = s.cwd || '';
|
||||||
sessions.push({
|
sessions.push({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.title || 'Untitled',
|
title: s.title || 'Untitled',
|
||||||
updated: s.updated,
|
updated: s.updated,
|
||||||
hasUnread: !!s.hasUnread,
|
hasUnread: !!s.hasUnread,
|
||||||
agent: getSessionAgent(s),
|
agent: getSessionAgent(s),
|
||||||
|
cwd,
|
||||||
|
projectName: cwd ? path.basename(cwd.replace(/[\\/]+$/, '')) : '',
|
||||||
isRunning: activeProcesses.has(s.id),
|
isRunning: activeProcesses.has(s.id),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
Reference in New Issue
Block a user