fix(agent): cancel session listing early
- commit
- e1d752edf3076d5d1f467e1511959b706fa93861
- author
- brice · · 2026-08-05 08:51 UTC
- parent
- ceda2c8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +40 -1
M packages/agent/modes/session_dialog.go +10 -1
| @@ -108,6 +108,15 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se | |||
| 108 | 108 | d.loadSlots = nil | |
| 109 | 109 | d.active = true | |
| 110 | 110 | ||
| 111 | + | if ctxErr := ctx.Err(); ctxErr != nil { | |
| 112 | + | cancel() | |
| 113 | + | d.loadCancel = nil | |
| 114 | + | d.loading = false | |
| 115 | + | events := make(chan sessionLoadEvent) | |
| 116 | + | close(events) | |
| 117 | + | return events | |
| 118 | + | } | |
| 119 | + | ||
| 111 | 120 | events := make(chan sessionLoadEvent, 1) | |
| 112 | 121 | send := func(event sessionLoadEvent) bool { | |
| 113 | 122 | select { | |
| @@ -124,7 +133,7 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se | |||
| 124 | 133 | return | |
| 125 | 134 | default: | |
| 126 | 135 | } | |
| 127 | - | paths := core.ListSessions(root, cwd) | |
| 136 | + | paths := core.ListSessionsContext(ctx, root, cwd) | |
| 128 | 137 | if !send(sessionLoadEvent{ | |
| 129 | 138 | kind: sessionLoadStarted, | |
| 130 | 139 | generation: generation, | |
| @@ -108,6 +108,15 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se | |||||
| 108 | d.loadSlots = nil | 108 | d.loadSlots = nil | ||
| 109 | d.active = true | 109 | d.active = true | ||
| 110 | 110 | ||||
| 111 | + | if ctxErr := ctx.Err(); ctxErr != nil { | |||
| 112 | + | cancel() | |||
| 113 | + | d.loadCancel = nil | |||
| 114 | + | d.loading = false | |||
| 115 | + | events := make(chan sessionLoadEvent) | |||
| 116 | + | close(events) | |||
| 117 | + | return events | |||
| 118 | + | } | |||
| 119 | + | ||||
| 111 | events := make(chan sessionLoadEvent, 1) | 120 | events := make(chan sessionLoadEvent, 1) | ||
| 112 | send := func(event sessionLoadEvent) bool { | 121 | send := func(event sessionLoadEvent) bool { | ||
| 113 | select { | 122 | select { | ||
| @@ -124,7 +133,7 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se | |||||
| 124 | return | 133 | return | ||
| 125 | default: | 134 | default: | ||
| 126 | } | 135 | } | ||
| 127 | - | paths := core.ListSessions(root, cwd) | 136 | + | paths := core.ListSessionsContext(ctx, root, cwd) |
| 128 | if !send(sessionLoadEvent{ | 137 | if !send(sessionLoadEvent{ | ||
| 129 | kind: sessionLoadStarted, | 138 | kind: sessionLoadStarted, | ||
| 130 | generation: generation, | 139 | generation: generation, | ||
M packages/agent/modes/session_dialog_test.go +14 -0
| @@ -61,6 +61,20 @@ func TestSessionDialogLoadsEntriesWithoutBlockingOpen(t *testing.T) { | |||
| 61 | 61 | } | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | + | func TestSessionDialogCanceledParentDoesNotRemainLoading(t *testing.T) { | |
| 65 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 66 | + | cancel() | |
| 67 | + | ||
| 68 | + | d := newSessionDialog() | |
| 69 | + | events := d.Open(ctx, t.TempDir(), t.TempDir()) | |
| 70 | + | if d.Loading() { | |
| 71 | + | t.Fatal("dialog remained loading with an already-canceled parent") | |
| 72 | + | } | |
| 73 | + | if _, ok := <-events; ok { | |
| 74 | + | t.Fatal("canceled dialog emitted a load event") | |
| 75 | + | } | |
| 76 | + | } | |
| 77 | + | ||
| 64 | 78 | func TestSessionDialogAppliesLoadedEntriesInPathOrder(t *testing.T) { | |
| 65 | 79 | d := newSessionDialog() | |
| 66 | 80 | d.active = true | |
| @@ -61,6 +61,20 @@ func TestSessionDialogLoadsEntriesWithoutBlockingOpen(t *testing.T) { | |||||
| 61 | } | 61 | } | ||
| 62 | } | 62 | } | ||
| 63 | 63 | ||||
| 64 | + | func TestSessionDialogCanceledParentDoesNotRemainLoading(t *testing.T) { | |||
| 65 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 66 | + | cancel() | |||
| 67 | + | ||||
| 68 | + | d := newSessionDialog() | |||
| 69 | + | events := d.Open(ctx, t.TempDir(), t.TempDir()) | |||
| 70 | + | if d.Loading() { | |||
| 71 | + | t.Fatal("dialog remained loading with an already-canceled parent") | |||
| 72 | + | } | |||
| 73 | + | if _, ok := <-events; ok { | |||
| 74 | + | t.Fatal("canceled dialog emitted a load event") | |||
| 75 | + | } | |||
| 76 | + | } | |||
| 77 | + | ||||
| 64 | func TestSessionDialogAppliesLoadedEntriesInPathOrder(t *testing.T) { | 78 | func TestSessionDialogAppliesLoadedEntriesInPathOrder(t *testing.T) { | ||
| 65 | d := newSessionDialog() | 79 | d := newSessionDialog() | ||
| 66 | d.active = true | 80 | d.active = true | ||
M packages/core/session.go +16 -0
| @@ -1075,6 +1075,16 @@ func sessionHasNoMessages(path string) bool { | |||
| 1075 | 1075 | // Files with identical ModTime fall back to filename desc so the | |
| 1076 | 1076 | // order stays stable across calls. | |
| 1077 | 1077 | func ListSessions(root, cwd string) []string { | |
| 1078 | + | return ListSessionsContext(context.Background(), root, cwd) | |
| 1079 | + | } | |
| 1080 | + | ||
| 1081 | + | // ListSessionsContext is the cancellation-aware form of ListSessions. It | |
| 1082 | + | // checks cancellation while walking directory entries so a closed picker does | |
| 1083 | + | // not continue preparing a large result set. | |
| 1084 | + | func ListSessionsContext(ctx context.Context, root, cwd string) []string { | |
| 1085 | + | if err := contextErr(ctx); err != nil { | |
| 1086 | + | return nil | |
| 1087 | + | } | |
| 1078 | 1088 | dir := SessionsDir(root, cwd) | |
| 1079 | 1089 | entries, err := os.ReadDir(dir) | |
| 1080 | 1090 | if err != nil { | |
| @@ -1086,6 +1096,9 @@ func ListSessions(root, cwd string) []string { | |||
| 1086 | 1096 | } | |
| 1087 | 1097 | var files []rec | |
| 1088 | 1098 | for _, e := range entries { | |
| 1099 | + | if err := contextErr(ctx); err != nil { | |
| 1100 | + | return nil | |
| 1101 | + | } | |
| 1089 | 1102 | if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { | |
| 1090 | 1103 | continue | |
| 1091 | 1104 | } | |
| @@ -1104,6 +1117,9 @@ func ListSessions(root, cwd string) []string { | |||
| 1104 | 1117 | }) | |
| 1105 | 1118 | out := make([]string, 0, len(files)) | |
| 1106 | 1119 | for _, r := range files { | |
| 1120 | + | if err := contextErr(ctx); err != nil { | |
| 1121 | + | return nil | |
| 1122 | + | } | |
| 1107 | 1123 | out = append(out, r.path) | |
| 1108 | 1124 | } | |
| 1109 | 1125 | return out | |
| @@ -1075,6 +1075,16 @@ func sessionHasNoMessages(path string) bool { | |||||
| 1075 | // Files with identical ModTime fall back to filename desc so the | 1075 | // Files with identical ModTime fall back to filename desc so the | ||
| 1076 | // order stays stable across calls. | 1076 | // order stays stable across calls. | ||
| 1077 | func ListSessions(root, cwd string) []string { | 1077 | func ListSessions(root, cwd string) []string { | ||
| 1078 | + | return ListSessionsContext(context.Background(), root, cwd) | |||
| 1079 | + | } | |||
| 1080 | + | ||||
| 1081 | + | // ListSessionsContext is the cancellation-aware form of ListSessions. It | |||
| 1082 | + | // checks cancellation while walking directory entries so a closed picker does | |||
| 1083 | + | // not continue preparing a large result set. | |||
| 1084 | + | func ListSessionsContext(ctx context.Context, root, cwd string) []string { | |||
| 1085 | + | if err := contextErr(ctx); err != nil { | |||
| 1086 | + | return nil | |||
| 1087 | + | } | |||
| 1078 | dir := SessionsDir(root, cwd) | 1088 | dir := SessionsDir(root, cwd) | ||
| 1079 | entries, err := os.ReadDir(dir) | 1089 | entries, err := os.ReadDir(dir) | ||
| 1080 | if err != nil { | 1090 | if err != nil { | ||
| @@ -1086,6 +1096,9 @@ func ListSessions(root, cwd string) []string { | |||||
| 1086 | } | 1096 | } | ||
| 1087 | var files []rec | 1097 | var files []rec | ||
| 1088 | for _, e := range entries { | 1098 | for _, e := range entries { | ||
| 1099 | + | if err := contextErr(ctx); err != nil { | |||
| 1100 | + | return nil | |||
| 1101 | + | } | |||
| 1089 | if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { | 1102 | if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") { | ||
| 1090 | continue | 1103 | continue | ||
| 1091 | } | 1104 | } | ||
| @@ -1104,6 +1117,9 @@ func ListSessions(root, cwd string) []string { | |||||
| 1104 | }) | 1117 | }) | ||
| 1105 | out := make([]string, 0, len(files)) | 1118 | out := make([]string, 0, len(files)) | ||
| 1106 | for _, r := range files { | 1119 | for _, r := range files { | ||
| 1120 | + | if err := contextErr(ctx); err != nil { | |||
| 1121 | + | return nil | |||
| 1122 | + | } | |||
| 1107 | out = append(out, r.path) | 1123 | out = append(out, r.path) | ||
| 1108 | } | 1124 | } | ||
| 1109 | return out | 1125 | return out | ||