feat: group sidebar sessions by project
This commit is contained in:
177
public/app.js
177
public/app.js
@@ -1288,6 +1288,109 @@
|
||||
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() {
|
||||
if (!chatCwd) return;
|
||||
if (currentCwd) {
|
||||
@@ -1754,6 +1857,16 @@
|
||||
case 'session_info':
|
||||
if (pendingNewSessionRequest) pendingNewSessionRequest = null;
|
||||
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) {
|
||||
activeSessionLoad.snapshot = snapshot;
|
||||
}
|
||||
@@ -3135,53 +3248,29 @@
|
||||
return;
|
||||
}
|
||||
|
||||
for (const s of visibleSessions) {
|
||||
const item = document.createElement('div');
|
||||
item.className = `session-item${s.id === currentSessionId ? ' active' : ''}`;
|
||||
item.dataset.id = s.id;
|
||||
item.innerHTML = `
|
||||
<div class="session-item-main">
|
||||
<span class="session-item-title">${escapeHtml(s.title || 'Untitled')}</span>
|
||||
${s.isRunning ? '<span class="session-item-status">运行中</span>' : ''}
|
||||
</div>
|
||||
${s.hasUnread ? '<span class="session-unread-dot"></span>' : ''}
|
||||
<span class="session-item-time">${timeAgo(s.updated)}</span>
|
||||
<div class="session-item-actions">
|
||||
<button class="session-item-btn edit" title="重命名">✎</button>
|
||||
<button class="session-item-btn delete" title="删除">×</button>
|
||||
</div>
|
||||
const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(visibleSessions);
|
||||
for (const group of projectGroups) {
|
||||
const groupEl = document.createElement('section');
|
||||
groupEl.className = 'session-project-group';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'session-project-header';
|
||||
header.title = group.cwd || group.name;
|
||||
header.innerHTML = `
|
||||
<span class="session-project-name">${escapeHtml(group.name)}</span>
|
||||
<span class="session-project-count">${group.sessions.length}</span>
|
||||
`;
|
||||
groupEl.appendChild(header);
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
const target = e.target;
|
||||
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);
|
||||
});
|
||||
for (const s of group.sessions) {
|
||||
groupEl.appendChild(createSessionListItem(s));
|
||||
}
|
||||
|
||||
sessionList.appendChild(item);
|
||||
sessionList.appendChild(groupEl);
|
||||
}
|
||||
|
||||
for (const s of ungroupedSessions) {
|
||||
sessionList.appendChild(createSessionListItem(s));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user