From a2126f413850f5f63973eb6dfb994843f8760693 Mon Sep 17 00:00:00 2001 From: shiyue Date: Thu, 18 Jun 2026 09:18:53 +0800 Subject: [PATCH] feat: add sidebar project collapse and search --- public/app.js | 139 +++++++++++++++++++++++++++++++++++++--- public/index.html | 4 ++ public/style.css | 160 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 295 insertions(+), 8 deletions(-) diff --git a/public/app.js b/public/app.js index 8bff990..7630aaa 100644 --- a/public/app.js +++ b/public/app.js @@ -7,6 +7,7 @@ const RENDER_DEBOUNCE = 100; const COMPOSER_SUGGESTION_DEBOUNCE = 120; const DIVIDER_TIME_STORAGE_KEY = 'cc-web-show-divider-time'; + const PROJECT_COLLAPSE_STORAGE_KEY = 'cc-web-collapsed-projects'; const SLASH_COMMANDS = [ { cmd: '/clear', desc: '清除当前会话' }, @@ -166,6 +167,15 @@ let noteMode = false; let noteDraftSeq = 0; let isReloadingMcp = false; + let sessionSearchQuery = ''; + const collapsedProjectKeys = (() => { + try { + const parsed = JSON.parse(localStorage.getItem(PROJECT_COLLAPSE_STORAGE_KEY) || '[]'); + return new Set(Array.isArray(parsed) ? parsed.filter(Boolean) : []); + } catch { + return new Set(); + } + })(); const pendingNotesByTarget = new Map(); const userMessageIndex = new Map(); const expandedOldSessionAgents = new Set(); @@ -190,6 +200,8 @@ const newChatArrow = $('#new-chat-arrow'); const newChatDropdown = $('#new-chat-dropdown'); const importSessionBtn = $('#import-session-btn'); + const sessionSearchInput = $('#session-search-input'); + const sessionSearchClear = $('#session-search-clear'); const sessionList = $('#session-list'); const chatTitle = $('#chat-title'); const chatSessionIdBtn = $('#chat-session-id-btn'); @@ -2272,6 +2284,61 @@ return sessions.filter((s) => normalizeAgent(s.agent) === currentAgent); } + function normalizeSessionSearchQuery(query) { + return String(query || '').trim().toLowerCase(); + } + + function syncSessionSearchUi() { + if (!sessionSearchInput) return; + if (sessionSearchInput.value !== sessionSearchQuery) { + sessionSearchInput.value = sessionSearchQuery; + } + const hasQuery = !!normalizeSessionSearchQuery(sessionSearchQuery); + sessionSearchInput.classList.toggle('has-value', hasQuery); + if (sessionSearchClear) { + sessionSearchClear.hidden = !hasQuery; + sessionSearchClear.disabled = !hasQuery; + } + } + + function getSessionSearchText(session) { + const cwd = getSessionEffectiveCwd(session); + return [ + session?.title, + getSessionProjectName(session), + cwd, + session?.id, + shortSessionId(session?.id), + ].filter(Boolean).join('\n').toLowerCase(); + } + + function sessionMatchesSearch(session, normalizedQuery) { + if (!normalizedQuery) return true; + return getSessionSearchText(session).includes(normalizedQuery); + } + + function getProjectCollapseKey(group) { + const rawKey = group?.cwd || group?.name || ''; + return `${normalizeAgent(currentAgent)}:${rawKey}`; + } + + function persistCollapsedProjectKeys() { + try { + localStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify([...collapsedProjectKeys])); + } catch {} + } + + function setProjectCollapsed(groupKey, collapsed) { + if (!groupKey) return; + if (collapsed) { + collapsedProjectKeys.add(groupKey); + } else { + collapsedProjectKeys.delete(groupKey); + } + persistCollapsedProjectKeys(); + renderSessionList(); + } + function getSessionCwdFromCache(sessionId) { if (!sessionId) return ''; const cachedCwd = sessionCache.get(sessionId)?.snapshot?.cwd; @@ -5361,17 +5428,32 @@ function renderSessionList() { sessionList.innerHTML = ''; - const visibleSessions = getVisibleSessions(); - if (visibleSessions.length === 0) { + syncSessionSearchUi(); + const allVisibleSessions = getVisibleSessions(); + const normalizedSearchQuery = normalizeSessionSearchQuery(sessionSearchQuery); + const isSearchingSessions = !!normalizedSearchQuery; + const visibleSessions = isSearchingSessions + ? allVisibleSessions.filter((session) => sessionMatchesSearch(session, normalizedSearchQuery)) + : allVisibleSessions; + if (allVisibleSessions.length === 0) { const empty = document.createElement('div'); empty.className = 'session-list-empty'; empty.textContent = `暂无 ${AGENT_LABELS[currentAgent]} 会话,点击“新会话”开始。`; sessionList.appendChild(empty); return; } + if (visibleSessions.length === 0) { + const empty = document.createElement('div'); + empty.className = 'session-list-empty'; + empty.textContent = '没有匹配的会话或项目。'; + sessionList.appendChild(empty); + return; + } const { pinnedSessions, regularSessions } = splitPinnedSessions(visibleSessions); - const { visibleRegularSessions, hiddenOldSessions } = splitCollapsedOldSessions(regularSessions, pinnedSessions.length); + const { visibleRegularSessions, hiddenOldSessions } = isSearchingSessions + ? { visibleRegularSessions: regularSessions, hiddenOldSessions: [] } + : splitCollapsedOldSessions(regularSessions, pinnedSessions.length); if (pinnedSessions.length > 0) { const pinnedGroupEl = document.createElement('section'); pinnedGroupEl.className = 'session-project-group session-pinned-group'; @@ -5391,15 +5473,24 @@ } const { groups: projectGroups, ungroupedSessions } = groupSessionsByProject(visibleRegularSessions); - for (const group of projectGroups) { + projectGroups.forEach((group, groupIndex) => { + const groupKey = getProjectCollapseKey(group); + const isCollapsed = !isSearchingSessions && collapsedProjectKeys.has(groupKey); + const hasActiveSession = group.sessions.some((session) => session.id === currentSessionId); + const hasUnreadSession = group.sessions.some((session) => session.hasUnread); + const hasRunningSession = group.sessions.some((session) => session.isRunning); + const groupBodyId = `session-project-body-${groupIndex}`; const groupEl = document.createElement('section'); - groupEl.className = 'session-project-group'; + groupEl.className = `session-project-group${isCollapsed ? ' collapsed' : ''}${hasActiveSession ? ' has-active-session' : ''}${hasUnreadSession ? ' has-unread-session' : ''}${hasRunningSession ? ' has-running-session' : ''}`; const header = document.createElement('div'); header.className = 'session-project-header'; header.title = group.cwd || group.name; header.innerHTML = ` - ${escapeHtml(group.name)} + ${group.sessions.length} @@ -5407,9 +5498,18 @@ `; groupEl.appendChild(header); + const groupBody = document.createElement('div'); + groupBody.id = groupBodyId; + groupBody.className = 'session-project-sessions'; + groupBody.hidden = isCollapsed; for (const s of group.sessions) { - groupEl.appendChild(createSessionListItem(s)); + groupBody.appendChild(createSessionListItem(s)); } + groupEl.appendChild(groupBody); + + header.querySelector('.session-project-toggle').addEventListener('click', () => { + setProjectCollapsed(groupKey, !isCollapsed); + }); header.querySelector('.session-project-create').addEventListener('click', (e) => { e.stopPropagation(); @@ -5417,7 +5517,7 @@ }); sessionList.appendChild(groupEl); - } + }); for (const s of ungroupedSessions) { sessionList.appendChild(createSessionListItem(s)); @@ -6083,6 +6183,29 @@ }); } + if (sessionSearchInput) { + sessionSearchInput.addEventListener('input', () => { + sessionSearchQuery = sessionSearchInput.value; + renderSessionList(); + }); + sessionSearchInput.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && normalizeSessionSearchQuery(sessionSearchQuery)) { + e.stopPropagation(); + sessionSearchQuery = ''; + renderSessionList(); + sessionSearchInput.focus(); + } + }); + } + + if (sessionSearchClear) { + sessionSearchClear.addEventListener('click', () => { + sessionSearchQuery = ''; + renderSessionList(); + sessionSearchInput?.focus(); + }); + } + // Split new-chat button newChatBtn.addEventListener('click', () => showNewSessionModal()); newChatArrow.addEventListener('click', (e) => { diff --git a/public/index.html b/public/index.html index fc64209..d531116 100644 --- a/public/index.html +++ b/public/index.html @@ -47,6 +47,10 @@ +