core: sort sessions by last-modified instead of filename
ListSessions sorted by filename descending. Filenames embed the creation timestamp, so /sessions, --continue, and the resume picker were effectively ordered by when each session was first created, not when the user last worked in it. Sort by filesystem ModTime descending instead, with filename desc as a stable tie-break. The session you most recently touched now sits at the top of every picker and is what --continue resumes, even when newer-but-idle sessions exist.
- commit
- 0cc003639c1c133ba38c669538041381720fa1ba
- author
- patriceckhart · · 2026-04-28 17:13 UTC
- parent
- 9f5f195
- signature
- unsigned
- refs
- tag: v0.1.40
1 file · +32 -6
M internal/core/session.go +32 -6
| @@ -443,21 +443,47 @@ func sessionHasNoMessages(path string) bool { | |||
| 443 | 443 | return true | |
| 444 | 444 | } | |
| 445 | 445 | ||
| 446 | - | // ListSessions returns session file paths for cwd, newest first. | |
| 446 | + | // ListSessions returns session file paths for cwd, most-recently- | |
| 447 | + | // modified first. Sorting on filesystem ModTime instead of the | |
| 448 | + | // timestamp embedded in the filename means a long-running session | |
| 449 | + | // the user actually returned to recently floats to the top of | |
| 450 | + | // /sessions, /continue, and the resume picker, even when it was | |
| 451 | + | // originally created days earlier than newer but idle sessions. | |
| 452 | + | // Files with identical ModTime fall back to filename desc so the | |
| 453 | + | // order stays stable across calls. | |
| 447 | 454 | func ListSessions(root, cwd string) []string { | |
| 448 | 455 | dir := SessionsDir(root, cwd) | |
| 449 | 456 | entries, err := os.ReadDir(dir) | |
| 450 | 457 | if err != nil { | |
| 451 | 458 | return nil | |
| 452 | 459 | } | |
| 453 | - | var files []string | |
| 460 | + | type rec struct { | |
| 461 | + | path string | |
| 462 | + | mod time.Time | |
| 463 | + | } | |
| 464 | + | var files []rec | |
| 454 | 465 | for _, e := range entries { | |
| 455 | - | if !e.IsDir() && strings.HasSuffix(e.Name(), ".jsonl") { | |
| 456 | - | files = append(files, filepath.Join(dir, e.Name())) | |
| 466 | + | if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { | |
| 467 | + | continue | |
| 457 | 468 | } | |
| 469 | + | p := filepath.Join(dir, e.Name()) | |
| 470 | + | info, err := e.Info() | |
| 471 | + | if err != nil { | |
| 472 | + | continue | |
| 473 | + | } | |
| 474 | + | files = append(files, rec{path: p, mod: info.ModTime()}) | |
| 475 | + | } | |
| 476 | + | sort.Slice(files, func(i, j int) bool { | |
| 477 | + | if !files[i].mod.Equal(files[j].mod) { | |
| 478 | + | return files[i].mod.After(files[j].mod) | |
| 479 | + | } | |
| 480 | + | return files[i].path > files[j].path | |
| 481 | + | }) | |
| 482 | + | out := make([]string, 0, len(files)) | |
| 483 | + | for _, r := range files { | |
| 484 | + | out = append(out, r.path) | |
| 458 | 485 | } | |
| 459 | - | sort.Sort(sort.Reverse(sort.StringSlice(files))) | |
| 460 | - | return files | |
| 486 | + | return out | |
| 461 | 487 | } | |
| 462 | 488 | ||
| 463 | 489 | // AppendMessage writes a message to the session. | |
| @@ -443,21 +443,47 @@ func sessionHasNoMessages(path string) bool { | |||||
| 443 | return true | 443 | return true | ||
| 444 | } | 444 | } | ||
| 445 | 445 | ||||
| 446 | - | // ListSessions returns session file paths for cwd, newest first. | 446 | + | // ListSessions returns session file paths for cwd, most-recently- |
| 447 | + | // modified first. Sorting on filesystem ModTime instead of the | |||
| 448 | + | // timestamp embedded in the filename means a long-running session | |||
| 449 | + | // the user actually returned to recently floats to the top of | |||
| 450 | + | // /sessions, /continue, and the resume picker, even when it was | |||
| 451 | + | // originally created days earlier than newer but idle sessions. | |||
| 452 | + | // Files with identical ModTime fall back to filename desc so the | |||
| 453 | + | // order stays stable across calls. | |||
| 447 | func ListSessions(root, cwd string) []string { | 454 | func ListSessions(root, cwd string) []string { | ||
| 448 | dir := SessionsDir(root, cwd) | 455 | dir := SessionsDir(root, cwd) | ||
| 449 | entries, err := os.ReadDir(dir) | 456 | entries, err := os.ReadDir(dir) | ||
| 450 | if err != nil { | 457 | if err != nil { | ||
| 451 | return nil | 458 | return nil | ||
| 452 | } | 459 | } | ||
| 453 | - | var files []string | 460 | + | type rec struct { |
| 461 | + | path string | |||
| 462 | + | mod time.Time | |||
| 463 | + | } | |||
| 464 | + | var files []rec | |||
| 454 | for _, e := range entries { | 465 | for _, e := range entries { | ||
| 455 | - | if !e.IsDir() && strings.HasSuffix(e.Name(), ".jsonl") { | 466 | + | if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { |
| 456 | - | files = append(files, filepath.Join(dir, e.Name())) | 467 | + | continue |
| 457 | } | 468 | } | ||
| 469 | + | p := filepath.Join(dir, e.Name()) | |||
| 470 | + | info, err := e.Info() | |||
| 471 | + | if err != nil { | |||
| 472 | + | continue | |||
| 473 | + | } | |||
| 474 | + | files = append(files, rec{path: p, mod: info.ModTime()}) | |||
| 475 | + | } | |||
| 476 | + | sort.Slice(files, func(i, j int) bool { | |||
| 477 | + | if !files[i].mod.Equal(files[j].mod) { | |||
| 478 | + | return files[i].mod.After(files[j].mod) | |||
| 479 | + | } | |||
| 480 | + | return files[i].path > files[j].path | |||
| 481 | + | }) | |||
| 482 | + | out := make([]string, 0, len(files)) | |||
| 483 | + | for _, r := range files { | |||
| 484 | + | out = append(out, r.path) | |||
| 458 | } | 485 | } | ||
| 459 | - | sort.Sort(sort.Reverse(sort.StringSlice(files))) | 486 | + | return out |
| 460 | - | return files | |||
| 461 | } | 487 | } | ||
| 462 | 488 | ||||
| 463 | // AppendMessage writes a message to the session. | 489 | // AppendMessage writes a message to the session. | ||