From cb7db53445e4b4cdfb45f696a5278321d8f5c115 Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Sat, 18 Apr 2026 11:41:54 +0900 Subject: [PATCH 1/4] feat(web): add collapsible archived section in session list Within each machine group, archived project groups are now separated into a collapsible "Archived (N)" section that defaults to collapsed. This reduces scroll distance when working across multiple machines. The section only appears when both active and archived groups exist in the same machine. Selecting an archived session auto-expands the section. --- web/src/components/SessionList.tsx | 164 ++++++++++++++++++++++------- web/src/lib/locales/en.ts | 1 + web/src/lib/locales/zh-CN.ts | 1 + 3 files changed, 129 insertions(+), 37 deletions(-) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 115cc266e..c46532024 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -472,6 +472,52 @@ function SessionItem(props: { ) } +function ProjectGroupItem(props: { + group: SessionGroup + isCollapsed: boolean + onToggle: () => void + onSelect: (sessionId: string) => void + api: ApiClient | null + selectedSessionId?: string | null +}) { + const { group, isCollapsed, onToggle, onSelect, api, selectedSessionId } = props + return ( +
+
+ + + {group.displayName} + + + + ({group.sessions.length}) + +
+ +
+
+
+ {group.sessions.map((s) => ( + + ))} +
+
+
+
+ ) +} + export function SessionList(props: { sessions: SessionSummary[] onSelect: (sessionId: string) => void @@ -492,6 +538,19 @@ export function SessionList(props: { const [collapseOverrides, setCollapseOverrides] = useState>( () => new Map() ) + const [archivedCollapsed, setArchivedCollapsed] = useState>( + () => new Map() + ) + const isArchivedSectionCollapsed = (machineKey: string): boolean => { + return archivedCollapsed.get(machineKey) ?? true + } + const toggleArchivedSection = (machineKey: string) => { + setArchivedCollapsed(prev => { + const next = new Map(prev) + next.set(machineKey, !isArchivedSectionCollapsed(machineKey)) + return next + }) + } const isGroupCollapsed = (group: SessionGroup): boolean => { const override = collapseOverrides.get(group.key) if (override !== undefined) return override @@ -567,6 +626,19 @@ export function SessionList(props: { } return changed ? next : prev }) + // Auto-expand archived section if selected session is in an archived group + const group = groups.find(g => + g.sessions.some(s => s.id === selectedSessionId) + ) + if (group && !group.hasActiveSession) { + const machineKey = group.machineId ?? UNKNOWN_MACHINE_ID + setArchivedCollapsed(prev => { + if (prev.get(machineKey) === false) return prev + const next = new Map(prev) + next.set(machineKey, false) + return next + }) + } }, [selectedSessionId, groups]) // Clean up stale collapse overrides @@ -629,45 +701,63 @@ export function SessionList(props: {
- {mg.projectGroups.map((group) => { - const isCollapsed = isGroupCollapsed(group) + {(() => { + const activeProjectGroups = mg.projectGroups.filter(g => g.hasActiveSession) + const archivedProjectGroups = mg.projectGroups.filter(g => !g.hasActiveSession) + const showArchivedSection = activeProjectGroups.length > 0 && archivedProjectGroups.length > 0 + const machineKey = mg.machineId ?? UNKNOWN_MACHINE_ID + const archivedHidden = showArchivedSection && isArchivedSectionCollapsed(machineKey) + const visibleGroups = showArchivedSection ? activeProjectGroups : mg.projectGroups + return ( -
-
toggleGroup(group.key, isCollapsed)} - title={group.directory} - > - - - {group.displayName} - - - - ({group.sessions.length}) - -
- - {/* Level 3: Sessions */} -
-
-
- {group.sessions.map((s) => ( - - ))} -
-
-
-
+ <> + {visibleGroups.map((group) => { + const isCollapsed = isGroupCollapsed(group) + return ( + toggleGroup(group.key, isCollapsed)} + onSelect={props.onSelect} + api={api} + selectedSessionId={selectedSessionId} + /> + ) + })} + {showArchivedSection ? ( + <> + +
+
+ {archivedProjectGroups.map((group) => { + const isCollapsed = isGroupCollapsed(group) + return ( + toggleGroup(group.key, isCollapsed)} + onSelect={props.onSelect} + api={api} + selectedSessionId={selectedSessionId} + /> + ) + })} +
+
+ + ) : null} + ) - })} + })()}
diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 77b5e1fe0..b0ea32b74 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -40,6 +40,7 @@ export default { // Sessions page 'sessions.count': '{n} sessions in {m} projects', + 'sessions.archived': 'Archived ({n})', 'sessions.new': 'New Session', // Session list diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index ca698dce3..d0ada89a4 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -40,6 +40,7 @@ export default { // Sessions page 'sessions.count': '{n} 个会话,{m} 个项目', + 'sessions.archived': '已归档 ({n})', 'sessions.new': '新建会话', // Session list From 611fcbfedd0fa5d272cce8e1b88e140b881530b7 Mon Sep 17 00:00:00 2001 From: Junmo Kim Date: Sat, 18 Apr 2026 11:44:38 +0900 Subject: [PATCH 2/4] fix(web): address review issues in archive collapse - Fix stale closure in toggleArchivedSection: read from prev instead of render-time state - Add aria-expanded to archived section toggle button - Change ProjectGroupItem header from div to button for keyboard accessibility - Add cleanup for stale archivedCollapsed entries when groups change --- web/src/components/SessionList.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index c46532024..4d59ca06b 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -483,7 +483,8 @@ function ProjectGroupItem(props: { const { group, isCollapsed, onToggle, onSelect, api, selectedSessionId } = props return (
-
({group.sessions.length}) -
+
@@ -547,7 +548,8 @@ export function SessionList(props: { const toggleArchivedSection = (machineKey: string) => { setArchivedCollapsed(prev => { const next = new Map(prev) - next.set(machineKey, !isArchivedSectionCollapsed(machineKey)) + const currentlyCollapsed = prev.get(machineKey) ?? true + next.set(machineKey, !currentlyCollapsed) return next }) } @@ -660,6 +662,19 @@ export function SessionList(props: { } return changed ? next : prev }) + setArchivedCollapsed(prev => { + if (prev.size === 0) return prev + const knownMachines = new Set(groups.map(g => g.machineId ?? UNKNOWN_MACHINE_ID)) + const next = new Map(prev) + let changed = false + for (const key of next.keys()) { + if (!knownMachines.has(key)) { + next.delete(key) + changed = true + } + } + return changed ? next : prev + }) }, [groups]) return ( @@ -729,6 +744,7 @@ export function SessionList(props: { <> - - ({group.sessions.length}) - - +