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"