diff --git a/packages/agent/modes/file_suggest_test.go b/packages/agent/modes/file_suggest_test.go index a986c03..d9f0866 100644 --- a/packages/agent/modes/file_suggest_test.go +++ b/packages/agent/modes/file_suggest_test.go @@ -176,6 +176,54 @@ func TestFileSuggesterFuzzyMatch(t *testing.T) { } } +// TestFileSuggesterFlatBrowseIntoDirIgnoresStaleFilter reproduces the +// reported bug: in flat (non-recursive) mode, typing "@eda" to find a +// directory then opening it with Right must show that directory's +// contents, not re-apply the "eda" filter inside it (which matches +// nothing). The interactive layer clears the @-query when descending, +// so here we model that by browsing with Right and then matching an +// empty query against the new level. +func TestFileSuggesterFlatBrowseIntoDirIgnoresStaleFilter(t *testing.T) { + tmp := t.TempDir() + // eda/rjg/enk-1150 with a file inside, plus a sibling so the filter + // is meaningful at the top level. + if err := os.MkdirAll(filepath.Join(tmp, "eda", "rjg", "enk-1150"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tmp, "eda", "rjg", "enk-1150", "pipeline.py"), []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(tmp, "unrelated"), 0o755); err != nil { + t.Fatal(err) + } + + s := newFileSuggester() + s.SetCWD(tmp) // flat mode + + // Top level: "@eda" highlights the eda/ directory. Render populates + // lastMatches, which Right/Left act on (it runs every frame before + // key handling in the live UI). + s.lastMatches = s.matches("@eda") + if !containsEntry(s.lastMatches, "eda", true) { + t.Fatalf("@eda did not match eda/: %#v", s.lastMatches) + } + s.cursor = 0 // eda/ is the (only) match, selected. + + // Open it. After the interactive layer clears the query, the picker + // is browsing eda/ with an empty filter and must show rjg/. + if !s.Right() { + t.Fatal("Right() did not open eda/") + } + if got := s.matches("@"); !containsEntry(got, "rjg", true) { + t.Fatalf("after opening eda/, empty filter did not show rjg/: %#v", got) + } + // The stale filter would have shown nothing: confirm that's the + // behavior the fix avoids. + if got := s.matches("@eda"); len(got) != 0 { + t.Fatalf("stale @eda filter inside eda/ unexpectedly matched: %#v", got) + } +} + // TestFileSuggesterRecursiveMatch verifies recursive mode flattens the // tree and matches against the cwd-relative path, so a pattern can // span directory boundaries. diff --git a/packages/agent/modes/interactive.go b/packages/agent/modes/interactive.go index f19dbdb..01db170 100644 --- a/packages/agent/modes/interactive.go +++ b/packages/agent/modes/interactive.go @@ -1605,6 +1605,18 @@ func (i *Interactive) ctrlCExitArmed() bool { return !t.IsZero() && time.Since(t) <= ctrlCExitWindow } +// clearFileSuggestQuery strips the filter the user typed after the +// last "@", leaving the bare "@" so the picker stays open. Called when +// navigating between directory levels (Right/Left): the filter applied +// to the level the user was on, not the one being entered, so carrying +// it forward would wrongly hide the new directory's contents. +func (i *Interactive) clearFileSuggestQuery() { + val := i.ed.Value() + if idx := strings.LastIndex(val, "@"); idx >= 0 { + i.ed.SetValue(val[:idx+1]) + } +} + func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { // Any key that isn't ctrl+c invalidates an armed ctrl+c-exit, so // pressing ctrl+c then typing then ctrl+c much later doesn't quit @@ -2083,12 +2095,21 @@ func (i *Interactive) handleKey(ctx context.Context, k tui.Key) (done bool) { i.fileSuggest.Down() return false case tui.KeyRight: - // Open selected directory. - i.fileSuggest.Right() + // Open selected directory. The filter the user typed picked + // that directory at the current level; once we descend it no + // longer applies to the directory's contents, so clear it. + // Otherwise typing "@eda" then right would re-filter inside + // eda/ by "eda" and show nothing. + if i.fileSuggest.Right() { + i.clearFileSuggestQuery() + } return false case tui.KeyLeft: - // Go back to parent directory. - i.fileSuggest.Left() + // Go back to parent directory. Clear the filter for the same + // reason as Right: it was scoped to the level we just left. + if i.fileSuggest.Left() { + i.clearFileSuggestQuery() + } return false case tui.KeyEnter: if entry, ok := i.fileSuggest.SelectedEntry(i.ed.Value()); ok {