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)}
+