From 087b9cf4e668e9245f4dd3ce25183d753e56a636 Mon Sep 17 00:00:00 2001 From: gaius-codius <206332531+gaius-codius@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:51:47 +0000 Subject: [PATCH 1/3] feat(web): group sessions by machine and improve group headers via [HAPI](https://hapi.run) Co-Authored-By: HAPI --- web/src/components/SessionList.tsx | 118 +++++++++++++++++++++-------- web/src/router.tsx | 24 +++++- 2 files changed, 110 insertions(+), 32 deletions(-) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 5131b98ae..fb97bed82 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -11,8 +11,10 @@ import { getSessionModelLabel } from '@/lib/sessionModelLabel' import { useTranslation } from '@/lib/use-translation' type SessionGroup = { + key: string directory: string displayName: string + machineId: string | null sessions: SessionSummary[] latestUpdatedAt: number hasActiveSession: boolean @@ -27,32 +29,46 @@ function getGroupDisplayName(directory: string): string { } function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] { - const groups = new Map() + const groups = new Map() sessions.forEach(session => { const path = session.metadata?.worktree?.basePath ?? session.metadata?.path ?? 'Other' - if (!groups.has(path)) { - groups.set(path, []) + const machineId = session.metadata?.machineId ?? null + const key = `${machineId ?? '__unknown__'}::${path}` + if (!groups.has(key)) { + groups.set(key, { + directory: path, + machineId, + sessions: [] + }) } - groups.get(path)!.push(session) + groups.get(key)!.sessions.push(session) }) return Array.from(groups.entries()) - .map(([directory, groupSessions]) => { - const sortedSessions = [...groupSessions].sort((a, b) => { + .map(([key, group]) => { + const sortedSessions = [...group.sessions].sort((a, b) => { const rankA = a.active ? (a.pendingRequestsCount > 0 ? 0 : 1) : 2 const rankB = b.active ? (b.pendingRequestsCount > 0 ? 0 : 1) : 2 if (rankA !== rankB) return rankA - rankB return b.updatedAt - a.updatedAt }) - const latestUpdatedAt = groupSessions.reduce( + const latestUpdatedAt = group.sessions.reduce( (max, s) => (s.updatedAt > max ? s.updatedAt : max), -Infinity ) - const hasActiveSession = groupSessions.some(s => s.active) - const displayName = getGroupDisplayName(directory) + const hasActiveSession = group.sessions.some(s => s.active) + const displayName = getGroupDisplayName(group.directory) - return { directory, displayName, sessions: sortedSessions, latestUpdatedAt, hasActiveSession } + return { + key, + directory: group.directory, + displayName, + machineId: group.machineId, + sessions: sortedSessions, + latestUpdatedAt, + hasActiveSession + } }) .sort((a, b) => { if (a.hasActiveSession !== b.hasActiveSession) { @@ -148,6 +164,27 @@ function getAgentLabel(session: SessionSummary): string { return 'unknown' } +function MachineIcon(props: { className?: string }) { + return ( + + + + + + ) +} + function formatRelativeTime(value: number, t: (key: string, params?: Record) => string): string | null { const ms = value < 1_000_000_000_000 ? value * 1000 : value if (!Number.isFinite(ms)) return null @@ -323,10 +360,11 @@ export function SessionList(props: { isLoading: boolean renderHeader?: boolean api: ApiClient | null + machineLabelsById?: Record selectedSessionId?: string | null }) { const { t } = useTranslation() - const { renderHeader = true, api, selectedSessionId } = props + const { renderHeader = true, api, selectedSessionId, machineLabelsById = {} } = props const groups = useMemo( () => groupSessionsByDirectory(props.sessions), [props.sessions] @@ -335,28 +373,38 @@ export function SessionList(props: { () => new Map() ) const isGroupCollapsed = (group: SessionGroup): boolean => { - const override = collapseOverrides.get(group.directory) + const override = collapseOverrides.get(group.key) if (override !== undefined) return override return !group.hasActiveSession } - const toggleGroup = (directory: string, isCollapsed: boolean) => { + const toggleGroup = (groupKey: string, isCollapsed: boolean) => { setCollapseOverrides(prev => { const next = new Map(prev) - next.set(directory, !isCollapsed) + next.set(groupKey, !isCollapsed) return next }) } + const resolveMachineLabel = (machineId: string | null): string => { + if (machineId && machineLabelsById[machineId]) { + return machineLabelsById[machineId] + } + if (machineId) { + return machineId.slice(0, 8) + } + return t('machine.unknown') + } + useEffect(() => { setCollapseOverrides(prev => { if (prev.size === 0) return prev const next = new Map(prev) - const knownGroups = new Set(groups.map(group => group.directory)) + const knownGroups = new Set(groups.map(group => group.key)) let changed = false - for (const directory of next.keys()) { - if (!knownGroups.has(directory)) { - next.delete(directory) + for (const groupKey of next.keys()) { + if (!knownGroups.has(groupKey)) { + next.delete(groupKey) changed = true } } @@ -385,28 +433,38 @@ export function SessionList(props: {
{groups.map((group) => { const isCollapsed = isGroupCollapsed(group) + const machineLabel = resolveMachineLabel(group.machineId) return ( -
+
{!isCollapsed ? ( -
+
{group.sessions.map((s) => ( { void refetch() }, [refetch]) - const projectCount = new Set(sessions.map(s => s.metadata?.worktree?.basePath ?? s.metadata?.path ?? 'Other')).size + const projectCount = new Set(sessions.map(s => { + const path = s.metadata?.worktree?.basePath ?? s.metadata?.path ?? 'Other' + const machineId = s.metadata?.machineId ?? '__unknown__' + return `${machineId}::${path}` + })).size + const machineLabelsById = useMemo(() => { + const labels: Record = {} + for (const machine of machines) { + labels[machine.id] = getMachineTitle(machine) + } + return labels + }, [machines]) const sessionMatch = matchRoute({ to: '/sessions/$sessionId', fuzzy: true }) const selectedSessionId = sessionMatch && sessionMatch.sessionId !== 'new' ? sessionMatch.sessionId : null const isSessionsIndex = pathname === '/sessions' || pathname === '/sessions/' @@ -160,6 +179,7 @@ function SessionsPage() { isLoading={isLoading} renderHeader={false} api={api} + machineLabelsById={machineLabelsById} />
From 2ed8460163644ffdd2184314fa90a634e2f39f06 Mon Sep 17 00:00:00 2001 From: gaius-codius <206332531+gaius-codius@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:22:48 +0000 Subject: [PATCH 2/3] fix: keep sidebar group expanded when it contains the selected session Prevents the selected session from disappearing when its group auto-collapses due to the active session being in a different group. via [HAPI](https://hapi.run) --- web/src/components/SessionList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index fb97bed82..b347f3ad7 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -375,7 +375,10 @@ export function SessionList(props: { const isGroupCollapsed = (group: SessionGroup): boolean => { const override = collapseOverrides.get(group.key) if (override !== undefined) return override - return !group.hasActiveSession + const hasSelectedSession = selectedSessionId + ? group.sessions.some(session => session.id === selectedSessionId) + : false + return !group.hasActiveSession && !hasSelectedSession } const toggleGroup = (groupKey: string, isCollapsed: boolean) => { From 02302d6f64d7dcb91ce20c4aa8e9f968d0d925e8 Mon Sep 17 00:00:00 2001 From: gaius-codius <206332531+gaius-codius@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:55:58 +0000 Subject: [PATCH 3/3] fix: address review feedback on session machine grouping - Clear manual collapse override when navigating into a collapsed group - Memoize projectCount to avoid recomputing on every render - Fix projectCount to count unique directories (not machine+dir pairs) so "N projects" label remains accurate across machines - Extract shared UNKNOWN_MACHINE_ID constant to prevent drift - Replace IIFE with pre-computed variable for todoProgress via [HAPI](https://hapi.run) --- web/src/components/SessionList.tsx | 34 ++++++++++++++++++++---------- web/src/router.tsx | 8 +++---- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index b347f3ad7..8abb19343 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -28,13 +28,15 @@ function getGroupDisplayName(directory: string): string { return `${parts[parts.length - 2]}/${parts[parts.length - 1]}` } +export const UNKNOWN_MACHINE_ID = '__unknown__' + function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] { const groups = new Map() sessions.forEach(session => { const path = session.metadata?.worktree?.basePath ?? session.metadata?.path ?? 'Other' const machineId = session.metadata?.machineId ?? null - const key = `${machineId ?? '__unknown__'}::${path}` + const key = `${machineId ?? UNKNOWN_MACHINE_ID}::${path}` if (!groups.has(key)) { groups.set(key, { directory: path, @@ -240,6 +242,7 @@ function SessionItem(props: { const statusDotClass = s.active ? (s.thinking ? 'bg-[#007AFF]' : 'bg-[var(--app-badge-success-text)]') : 'bg-[var(--app-hint)]' + const todoProgress = getTodoProgress(s) return ( <>