From 6f23b79edf20e538c4244f5ac448243e42269bf6 Mon Sep 17 00:00:00 2001 From: "seungju24.choi" Date: Tue, 1 Sep 2026 07:24:37 +0900 Subject: [PATCH] keep typed query when opening a search result Accepting a search result with Enter committed a new search scoped to the picked task's display ref before opening its detail. Closing the detail then left a single-task search view instead of the result list the user was choosing from. Commit the typed query as the search view and select the picked task within it. When the picked result is missing from the committed results (the input changed after the preview ran), fall back to opening the task through the exact-task detail path. Regression coverage includes committing the typed query with the picked task selected and the exact-task fallback. --- CHANGELOG.md | 4 + src/tui/app_search.rs | 14 +++- .../app_tests/typed_overlay_submissions.rs | 73 +++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0bfa9dd..0a1b98ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ title: Changelog description: Release notes for aven. --- +## Unreleased + +- Fix: Opening a [search](https://aventasks.dev/tui/#search-filter-and-order) result with `Enter` keeps the typed query as the committed search view, so closing the task detail returns to the full result list instead of a single-task view scoped to that task's reference. + ## v0.1.43 (2026-09-26) - Fix: Moving tasks or recurring schedules to a project now requires that project to exist in the selected workspace, preventing accidental duplicate projects. ([#29](https://github.com/raine/aven/issues/29)) diff --git a/src/tui/app_search.rs b/src/tui/app_search.rs index 369f27ef..432b7e26 100644 --- a/src/tui/app_search.rs +++ b/src/tui/app_search.rs @@ -268,10 +268,16 @@ impl App { ) -> Result<()> { match intent { SearchIntent::Navigate => { - self.accept_search_input(result.display_ref.clone()).await?; - self.select_task_by_id(&result.task_id); - self.detail = crate::tui::detail_session::DetailSession::open(0); - self.show_detail(0); + self.accept_search_input(input).await?; + if self.select_task_by_id(&result.task_id) { + self.detail = crate::tui::detail_session::DetailSession::open(0); + self.show_detail(0); + } else { + // The picked result can be missing from the committed query + // results (e.g. the input changed after the preview ran); + // fall back to opening it as an exact task. + self.open_detail_task(&result.task_id, 0).await; + } } SearchIntent::AddDependency { selection, diff --git a/src/tui/app_tests/typed_overlay_submissions.rs b/src/tui/app_tests/typed_overlay_submissions.rs index 5eceb927..84b564d6 100644 --- a/src/tui/app_tests/typed_overlay_submissions.rs +++ b/src/tui/app_tests/typed_overlay_submissions.rs @@ -149,6 +149,79 @@ async fn search_selected_blocker_adds_dependency() { ); } +#[tokio::test] +async fn navigate_search_result_commits_typed_query() { + let mut app = test_app().await; + create_and_select_task(&mut app, test_task_draft("Needle one")).await; + let second = create_and_select_task(&mut app, test_task_draft("Needle two")).await; + let second_id = app.store.tasks[second].task.id.clone(); + + app.begin_search(); + type_chars(&mut app, "needle").await; + settle_search_preview(&mut app).await; + let Some(OverlayState::Search(state)) = &mut app.overlay else { + panic!("expected search overlay"); + }; + state.selected = state + .results + .iter() + .position(|result| result.task_id == second_id) + .expect("picked task in preview results"); + app.handle_overlay_key(key(KeyCode::Enter)).await.unwrap(); + + assert!(app.overlay.is_none()); + assert!(app.detail.is_active()); + assert_eq!(app.store.view_state.query, TaskQuery::Search); + assert!(matches!( + &app.store.view_state.projection_origin, + crate::tui::store::TaskProjectionOrigin::Search { query, .. } if query == "needle" + )); + assert_eq!(app.store.tasks.len(), 2); + let selected = app.list.selected_task().unwrap(); + assert_eq!(app.store.tasks[selected].task.id, second_id); +} + +#[tokio::test] +async fn navigate_search_result_missing_from_results_opens_exact_task() { + let mut app = test_app().await; + create_and_select_task(&mut app, test_task_draft("Alpha target")).await; + let other = create_and_select_task(&mut app, test_task_draft("Beta other")).await; + let other_id = app.store.tasks[other].task.id.clone(); + + app.overlay = Some(OverlayState::Search(SearchState { + input: LineEdit::new("alpha".to_string()), + results: vec![crate::tui::overlay::SearchResultItem { + task_id: other_id.clone(), + display_ref: "BETA-REF".to_string(), + title: "Beta other".to_string(), + description: String::new(), + project_key: String::new(), + status: "inbox".to_string(), + priority: "none".to_string(), + created_at: String::new(), + labels: Vec::new(), + matched_field: crate::query::SearchMatchedField::Title, + snippet: None, + score: 0, + deleted: false, + is_epic: false, + unavailable_reason: None, + create_new: false, + }], + selected: 0, + total_matches: 1, + results_query: Some("alpha".to_string()), + intent: SearchIntent::Navigate, + })); + app.handle_overlay_key(key(KeyCode::Enter)).await.unwrap(); + + assert!(app.overlay.is_none()); + assert!(app.detail.is_active()); + assert_eq!(app.store.tasks.len(), 1); + assert_eq!(app.store.tasks[0].task.id, other_id); + assert_eq!(app.list.selected_task(), Some(0)); +} + #[tokio::test] async fn add_dependency_search_tab_keeps_picker_context() { let mut app = test_app().await;