From a3e30abb70deb398dc10ac2866223e693010f695 Mon Sep 17 00:00:00 2001 From: webwww123 <197701451+webwww123@users.noreply.github.com> Date: Tue, 10 Mar 2026 05:50:59 +0100 Subject: [PATCH] feat(web): show machine label in session list --- web/src/components/SessionList.test.ts | 60 ++++++++++++++++++ web/src/components/SessionList.tsx | 80 +++++++----------------- web/src/components/sessionListUtils.ts | 86 ++++++++++++++++++++++++++ web/src/lib/locales/en.ts | 1 + web/src/lib/locales/zh-CN.ts | 1 + web/src/router.tsx | 2 + 6 files changed, 173 insertions(+), 57 deletions(-) create mode 100644 web/src/components/SessionList.test.ts create mode 100644 web/src/components/sessionListUtils.ts diff --git a/web/src/components/SessionList.test.ts b/web/src/components/SessionList.test.ts new file mode 100644 index 000000000..491065c05 --- /dev/null +++ b/web/src/components/SessionList.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' +import type { Machine, SessionSummary } from '@/types/api' +import { getMachineLabel, groupSessionsByDirectory } from './sessionListUtils' + +function createSession(overrides: Partial = {}): SessionSummary { + return { + id: overrides.id ?? 'session-1', + updatedAt: overrides.updatedAt ?? 1, + activeAt: overrides.activeAt ?? 1, + active: overrides.active ?? false, + pendingRequestsCount: overrides.pendingRequestsCount ?? 0, + thinking: overrides.thinking ?? false, + modelMode: overrides.modelMode ?? undefined, + todoProgress: overrides.todoProgress ?? null, + metadata: { + path: '/root/project-a', + machineId: 'machine-alpha-1234', + ...overrides.metadata, + }, + } +} + +describe('SessionList helpers', () => { + it('separates groups by machine when directory is the same', () => { + const machinesById = new Map([ + ['machine-alpha-1234', { id: 'machine-alpha-1234', active: true, metadata: { host: 'alpha-host', platform: 'linux', happyCliVersion: '0.16.1' } }], + ['machine-beta-5678', { id: 'machine-beta-5678', active: true, metadata: { host: 'beta-host', platform: 'linux', happyCliVersion: '0.16.1' } }], + ]) + const sessions = [ + createSession({ + id: 'a', + metadata: { + path: '/root/project-a', + machineId: 'machine-alpha-1234', + }, + }), + createSession({ + id: 'b', + metadata: { + path: '/root/project-a', + machineId: 'machine-beta-5678', + }, + }), + ] + + const groups = groupSessionsByDirectory(sessions, machinesById) + + expect(groups).toHaveLength(2) + expect(groups.map(group => group.machineLabel)).toEqual(['alpha-host', 'beta-host']) + }) + + it('falls back to a short machine id when host is unavailable', () => { + const label = getMachineLabel( + 'de5b5751-112d-42d4-b330-5b8ec3822cea', + new Map() + ) + + expect(label).toBe('de5b5751') + }) +}) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 69c71c37b..b58b55839 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -8,58 +8,9 @@ import { SessionActionMenu } from '@/components/SessionActionMenu' import { RenameSessionDialog } from '@/components/RenameSessionDialog' import { ConfirmDialog } from '@/components/ui/ConfirmDialog' import { useTranslation } from '@/lib/use-translation' - -type SessionGroup = { - directory: string - displayName: string - sessions: SessionSummary[] - latestUpdatedAt: number - hasActiveSession: boolean -} - -function getGroupDisplayName(directory: string): string { - if (directory === 'Other') return directory - const parts = directory.split(/[\\/]+/).filter(Boolean) - if (parts.length === 0) return directory - if (parts.length === 1) return parts[0] - return `${parts[parts.length - 2]}/${parts[parts.length - 1]}` -} - -function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] { - const groups = new Map() - - sessions.forEach(session => { - const path = session.metadata?.worktree?.basePath ?? session.metadata?.path ?? 'Other' - if (!groups.has(path)) { - groups.set(path, []) - } - groups.get(path)!.push(session) - }) - - return Array.from(groups.entries()) - .map(([directory, groupSessions]) => { - const sortedSessions = [...groupSessions].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( - (max, s) => (s.updatedAt > max ? s.updatedAt : max), - -Infinity - ) - const hasActiveSession = groupSessions.some(s => s.active) - const displayName = getGroupDisplayName(directory) - - return { directory, displayName, sessions: sortedSessions, latestUpdatedAt, hasActiveSession } - }) - .sort((a, b) => { - if (a.hasActiveSession !== b.hasActiveSession) { - return a.hasActiveSession ? -1 : 1 - } - return b.latestUpdatedAt - a.latestUpdatedAt - }) -} +import { getMachineLabel, groupSessionsByDirectory } from '@/components/sessionListUtils' +import type { SessionGroup } from '@/components/sessionListUtils' +import type { Machine } from '@/types/api' function PlusIcon(props: { className?: string }) { return ( @@ -163,13 +114,14 @@ function formatRelativeTime(value: number, t: (key: string, params?: Record void showPath?: boolean api: ApiClient | null selected?: boolean }) { const { t } = useTranslation() - const { session: s, onSelect, showPath = true, api, selected = false } = props + const { session: s, machineLabel, onSelect, showPath = true, api, selected = false } = props const { haptic } = usePlatform() const [menuOpen, setMenuOpen] = useState(false) const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) @@ -253,6 +205,9 @@ function SessionItem(props: { ) : null}
+ {machineLabel ? ( + {t('session.item.machine')}: {machineLabel} + ) : null}