diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 775969bfcb38..a3512c1de536 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -34,29 +34,88 @@ export function DialogSessionList() { const sessions = createMemo(() => searchResults() ?? sync.data.session) + function parseSessionTitle(title: string): { group?: string; displayTitle: string } { + const pipeIndex = title.indexOf("|") + if (pipeIndex === -1) { + return { displayTitle: title } + } + + const group = title.slice(0, pipeIndex).trim() + const displayTitle = title.slice(pipeIndex + 1).trim() + + if (!group) { + return { displayTitle } + } + + return { group, displayTitle } + } + const options = createMemo(() => { const today = new Date().toDateString() - return sessions() - .filter((x) => x.parentID === undefined) - .toSorted((a, b) => b.time.updated - a.time.updated) - .map((x) => { - const date = new Date(x.time.updated) - let category = date.toDateString() - if (category === today) { - category = "Today" - } - const isDeleting = toDelete() === x.id - const status = sync.data.session_status?.[x.id] - const isWorking = status?.type === "busy" - return { - title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title, - bg: isDeleting ? theme.error : undefined, - value: x.id, - category, - footer: Locale.time(x.time.updated), - gutter: isWorking ? : undefined, - } - }) + const allSessions = sessions().filter((x) => x.parentID === undefined) + + // Separate into grouped and ungrouped + const grouped: typeof allSessions = [] + const ungrouped: typeof allSessions = [] + + for (const session of allSessions) { + const parsed = parseSessionTitle(session.title) + if (parsed.group) { + grouped.push(session) + } else { + ungrouped.push(session) + } + } + + // Sort grouped by group name ASC, then updated DESC + grouped.sort((a, b) => { + const aParsed = parseSessionTitle(a.title) + const bParsed = parseSessionTitle(b.title) + const groupCompare = (aParsed.group ?? "").localeCompare(bParsed.group ?? "") + if (groupCompare !== 0) return groupCompare + return b.time.updated - a.time.updated + }) + + // Sort ungrouped by updated DESC + ungrouped.sort((a, b) => b.time.updated - a.time.updated) + + // Map grouped sessions + const groupedOptions = grouped.map((session) => { + const parsed = parseSessionTitle(session.title) + const isDeleting = toDelete() === session.id + const status = sync.data.session_status?.[session.id] + const isWorking = status?.type === "busy" + return { + title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : parsed.displayTitle, + bg: isDeleting ? theme.error : undefined, + value: session.id, + category: parsed.group, + footer: Locale.shortDateTime(session.time.updated), + gutter: isWorking ? : undefined, + } + }) + + // Map ungrouped sessions + const ungroupedOptions = ungrouped.map((session) => { + const date = new Date(session.time.updated) + let category = date.toDateString() + if (category === today) { + category = "Today" + } + const isDeleting = toDelete() === session.id + const status = sync.data.session_status?.[session.id] + const isWorking = status?.type === "busy" + return { + title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : session.title, + bg: isDeleting ? theme.error : undefined, + value: session.id, + category, + footer: Locale.time(session.time.updated), + gutter: isWorking ? : undefined, + } + }) + + return [...groupedOptions, ...ungroupedOptions] }) onMount(() => { diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index 653da09a0b7d..76e94dff164d 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -28,6 +28,27 @@ export namespace Locale { } } + export function shortDateTime(input: number): string { + const date = new Date(input) + const now = new Date() + const isToday = + date.getFullYear() === now.getFullYear() && + date.getMonth() === now.getMonth() && + date.getDate() === now.getDate() + + const timeStr = time(input) + + if (isToday) { + return timeStr + } else { + const dateStr = date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }) + return `${dateStr} ยท ${timeStr}` + } + } + export function number(num: number): string { if (num >= 1000000) { return (num / 1000000).toFixed(1) + "M"