detached e1d752e

fix(agent): cancel session listing early

commit
e1d752edf3076d5d1f467e1511959b706fa93861
author
brice · · 2026-08-05 08:51 UTC
parent
ceda2c8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
108108 d.loadSlots = nil
109109 d.active = true
110110
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+
111120 events := make(chan sessionLoadEvent, 1)
112121 send := func(event sessionLoadEvent) bool {
113122 select {
@@ -124,7 +133,7 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se
124133 return
125134 default:
126135 }
127- paths := core.ListSessions(root, cwd)
136+ paths := core.ListSessionsContext(ctx, root, cwd)
128137 if !send(sessionLoadEvent{
129138 kind: sessionLoadStarted,
130139 generation: generation,
@@ -108,6 +108,15 @@ func (d *sessionDialog) Open(parent context.Context, root, cwd string) <-chan se
108 d.loadSlots = nil108 d.loadSlots = nil
109 d.active = true109 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 return133 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) {
6161 }
6262 }
6363
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+
6478 func TestSessionDialogAppliesLoadedEntriesInPathOrder(t *testing.T) {
6579 d := newSessionDialog()
6680 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 = true80 d.active = true
M packages/core/session.go +16 -0
@@ -1075,6 +1075,16 @@ func sessionHasNoMessages(path string) bool {
10751075 // Files with identical ModTime fall back to filename desc so the
10761076 // order stays stable across calls.
10771077 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+ }
10781088 dir := SessionsDir(root, cwd)
10791089 entries, err := os.ReadDir(dir)
10801090 if err != nil {
@@ -1086,6 +1096,9 @@ func ListSessions(root, cwd string) []string {
10861096 }
10871097 var files []rec
10881098 for _, e := range entries {
1099+ if err := contextErr(ctx); err != nil {
1100+ return nil
1101+ }
10891102 if e.IsDir() || !strings.HasSuffix(e.Name(), ".jsonl") {
10901103 continue
10911104 }
@@ -1104,6 +1117,9 @@ func ListSessions(root, cwd string) []string {
11041117 })
11051118 out := make([]string, 0, len(files))
11061119 for _, r := range files {
1120+ if err := contextErr(ctx); err != nil {
1121+ return nil
1122+ }
11071123 out = append(out, r.path)
11081124 }
11091125 return out
@@ -1075,6 +1075,16 @@ func sessionHasNoMessages(path string) bool {
1075 // Files with identical ModTime fall back to filename desc so the1075 // 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 []rec1097 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 continue1103 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 out1125 return out

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close