diff --git a/docs/feature-checklist.md b/docs/feature-checklist.md index 1b63413..b7f9835 100644 --- a/docs/feature-checklist.md +++ b/docs/feature-checklist.md @@ -92,7 +92,7 @@ sidebar, insets + rounds the content area, and re-homes the toolbar off-screen. |---|---------|--------------------|------|----------------------| | ☐ | Cmd+T in sidebar window shows command bar instead of blank tab | `chrome/browser/ui/browser_commands.cc.patch` (`NewTab()` hook) | 🔴 | Cmd+T shows command bar; programmatic/restore new-tabs still create real tabs | | ☐ | Cmd+L pre-fills current URL | `views/frame/browser_view.cc.patch` (`SetFocusToLocationBar` redirect) | 🔴 | Cmd+L opens command bar with URL | -| ☐ | Command bar UI + suggestions + Ask AI | `src/dao/.../dao_command_bar_view.*`, `dao_suggestion_item_view.*` | `DaoCommandBarBrowserTest.RightArrowFillsExplicitlySelectedSuggestion` | Arrow-key select; Right Arrow fills the explicitly selected suggestion without navigating; Tab-complete; Esc dismiss; Ask AI routes to agent | +| ☐ | Command bar UI + suggestions + Ask AI | `src/dao/.../dao_command_bar_view.*`, `dao_suggestion_item_view.*` | `DaoCommandBarBrowserTest.RightArrowFillsExplicitlySelectedSuggestion`, `DaoCommandBarBrowserTest.ReservesExactSearchWhenNonSearchMatchesFillVisibleSlots`, `DaoCommandBarBrowserTest.ExactSearchTabMatchDoesNotReplaceReservedSearchAction`, `DaoCommandBarBrowserTest.EmptyInputShowsNoSuggestionsInBothModes`, `DaoCommandBarBrowserTest.WhitespaceOnlyInputShowsNoSuggestions` | Arrow-key select; Right Arrow fills the explicitly selected suggestion without navigating; Tab-complete; Esc dismiss; Ask AI routes to agent and remains second when eligible. Fill all competing suggestion slots and verify one exact-input Search action remains visible, including for URL-like input, and submits through the default search provider. Empty and whitespace-only input must show zero suggestions in both modes. | ## 3. AI Agent System diff --git a/docs/features.md b/docs/features.md index 0c7f18f..edf5ed3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -77,6 +77,9 @@ An Arc-inspired vertical sidebar replaces Chromium's top tab strip — the singl - Cmd+T → `ShowForNewTab(prev)` opens blank tab, remembers previous tab; Esc / click-outside calls `CancelNewTab()` to close the blank and return - **Ask AI** — Submits prompt directly to the Agent - URL-vs-query detection heuristics + ghost-text completion + - Every non-blank query reserves one exact-input Search action within the + five-row suggestion limit, even when history, tabs, bookmarks, or URL + matches rank above it; empty and whitespace-only input shows no suggestions - Keyboard-first: arrow keys to select, Right Arrow to fill the selected suggestion into the input, Tab to complete, Esc to dismiss - **DaoSuggestionItemView** (`dao_suggestion_item_view.{h,cc}`) — Suggestion row diff --git a/src/dao/browser/ui/views/dao_browser_browsertest.cc b/src/dao/browser/ui/views/dao_browser_browsertest.cc index 0b50b66..a45b959 100644 --- a/src/dao/browser/ui/views/dao_browser_browsertest.cc +++ b/src/dao/browser/ui/views/dao_browser_browsertest.cc @@ -288,6 +288,35 @@ bool HasDescendantLabelText(views::View* root, std::u16string_view text) { return false; } +int CountDescendantLabelsWithText(views::View* root, std::u16string_view text) { + int count = 0; + if (auto* label = views::AsViewClass(root); + label && label->GetText() == text) { + ++count; + } + for (views::View* child : root->children()) { + count += CountDescendantLabelsWithText(child, text); + } + return count; +} + +bool HasVisibleDescendantLabelText(views::View* root, + std::u16string_view text) { + if (!root || !root->GetVisible()) { + return false; + } + if (auto* label = views::AsViewClass(root); + label && label->GetText() == text) { + return true; + } + for (views::View* child : root->children()) { + if (HasVisibleDescendantLabelText(child, text)) { + return true; + } + } + return false; +} + views::Label* FindDescendantLabelWithText(views::View* root, std::u16string_view text) { if (!root) { @@ -3222,6 +3251,313 @@ IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, })); } +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + EmptyInputShowsNoSuggestionsInBothModes) { + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + + AutocompleteMatch history_match(nullptr, 1000, false, + AutocompleteMatchType::HISTORY_URL); + history_match.allowed_to_be_default_match = true; + history_match.fill_into_edit = u"github.com"; + history_match.contents = u"github.com"; + history_match.contents_class = { + {0, AutocompleteMatch::ACMatchClassification::URL}}; + history_match.destination_url = GURL("https://github.com/"); + + for (bool enhanced : {false, true}) { + browser()->profile()->GetPrefs()->SetBoolean( + dao::prefs::kDaoEnhancedCommandBarSuggestionsEnabled, enhanced); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"git", u""); + command_bar->SetAutocompleteMatchesForTesting(ACMatches{history_match}); + ASSERT_GT(command_bar->GetVisibleSuggestionCountForTesting(), 0); + + command_bar->ContentsChanged(nullptr, u""); + + EXPECT_EQ(0, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_EQ(-1, command_bar->GetSelectedIndexForTesting()); + EXPECT_EQ(-1, command_bar->GetAskAiRowIndexForTesting()); + EXPECT_FALSE( + HasVisibleDescendantLabelText(command_bar, u"github.com")); + command_bar->Hide(); + } +} + +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + WhitespaceOnlyInputShowsNoSuggestions) { + browser()->profile()->GetPrefs()->SetBoolean( + dao::prefs::kDaoEnhancedCommandBarSuggestionsEnabled, true); + + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"git", u""); + + AutocompleteMatch history_match(nullptr, 1000, false, + AutocompleteMatchType::HISTORY_URL); + history_match.allowed_to_be_default_match = true; + history_match.fill_into_edit = u"github.com"; + history_match.contents = u"github.com"; + history_match.contents_class = { + {0, AutocompleteMatch::ACMatchClassification::URL}}; + history_match.destination_url = GURL("https://github.com/"); + command_bar->SetAutocompleteMatchesForTesting(ACMatches{history_match}); + ASSERT_GT(command_bar->GetVisibleSuggestionCountForTesting(), 0); + + command_bar->ContentsChanged(nullptr, u" "); + + EXPECT_EQ(0, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_EQ(-1, command_bar->GetSelectedIndexForTesting()); + EXPECT_EQ(-1, command_bar->GetAskAiRowIndexForTesting()); + EXPECT_FALSE(HasVisibleDescendantLabelText(command_bar, u"github.com")); +} + +IN_PROC_BROWSER_TEST_F( + DaoCommandBarBrowserTest, + ReservesExactSearchWhenNonSearchMatchesFillVisibleSlots) { + browser()->profile()->GetPrefs()->SetBoolean( + dao::prefs::kDaoEnhancedCommandBarSuggestionsEnabled, true); + browser()->profile()->GetPrefs()->SetBoolean(dao::prefs::kDaoAskAiEnabled, + false); + + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"dao browser", + u""); + + ACMatches matches; + for (int i = 0; i < 5; ++i) { + AutocompleteMatch match(nullptr, 1000 - i, false, + AutocompleteMatchType::HISTORY_URL); + match.allowed_to_be_default_match = i == 0; + match.fill_into_edit = + base::ASCIIToUTF16("history" + base::NumberToString(i)); + match.contents = match.fill_into_edit; + match.contents_class = {{0, AutocompleteMatch::ACMatchClassification::URL}}; + match.destination_url = + GURL("https://example.com/" + base::NumberToString(i)); + matches.push_back(std::move(match)); + } + command_bar->SetAutocompleteMatchesForTesting(matches); + + EXPECT_EQ(5, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"dao browser")); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"Search")); + EXPECT_FALSE(HasDescendantLabelText(command_bar, u"history4")); +} + +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + DoesNotDuplicateVisibleExactSearchMatch) { + browser()->profile()->GetPrefs()->SetBoolean(dao::prefs::kDaoAskAiEnabled, + false); + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"dao browser", + u""); + + AutocompleteMatch search_match(nullptr, 1000, false, + AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); + search_match.allowed_to_be_default_match = true; + search_match.fill_into_edit = u"dao browser"; + search_match.contents = u"dao browser"; + search_match.contents_class = { + {0, AutocompleteMatch::ACMatchClassification::NONE}}; + search_match.destination_url = + GURL("https://www.google.com/search?q=dao+browser"); + + ACMatches matches{search_match}; + for (int i = 0; i < 4; ++i) { + AutocompleteMatch match(nullptr, 900 - i, false, + AutocompleteMatchType::HISTORY_URL); + match.fill_into_edit = + base::ASCIIToUTF16("history" + base::NumberToString(i)); + match.contents = match.fill_into_edit; + match.destination_url = + GURL("https://example.com/" + base::NumberToString(i)); + matches.push_back(std::move(match)); + } + command_bar->SetAutocompleteMatchesForTesting(matches); + + EXPECT_EQ(5, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_EQ(1, CountDescendantLabelsWithText(command_bar, u"dao browser")); +} + +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + PromotesExactSearchBelowVisibleCutoff) { + browser()->profile()->GetPrefs()->SetBoolean(dao::prefs::kDaoAskAiEnabled, + false); + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"dao browser", + u""); + + ACMatches matches; + for (int i = 0; i < 5; ++i) { + AutocompleteMatch match(nullptr, 1000 - i, false, + AutocompleteMatchType::HISTORY_URL); + match.allowed_to_be_default_match = i == 0; + match.fill_into_edit = + base::ASCIIToUTF16("history" + base::NumberToString(i)); + match.contents = match.fill_into_edit; + match.destination_url = + GURL("https://example.com/" + base::NumberToString(i)); + matches.push_back(std::move(match)); + } + AutocompleteMatch search_match(nullptr, 100, false, + AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); + search_match.fill_into_edit = u"dao browser"; + search_match.contents = u"dao browser"; + search_match.destination_url = + GURL("https://www.google.com/search?q=dao+browser"); + matches.push_back(std::move(search_match)); + command_bar->SetAutocompleteMatchesForTesting(matches); + + EXPECT_EQ(5, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"dao browser")); + EXPECT_FALSE(HasDescendantLabelText(command_bar, u"history4")); +} + +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + ReservesSearchForUrlLikeInput) { + browser()->profile()->GetPrefs()->SetBoolean(dao::prefs::kDaoAskAiEnabled, + false); + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"example.com", + u""); + + AutocompleteMatch history_match(nullptr, 1000, false, + AutocompleteMatchType::HISTORY_URL); + history_match.allowed_to_be_default_match = true; + history_match.fill_into_edit = u"https://example.com"; + history_match.contents = u"example.com"; + history_match.destination_url = GURL("https://example.com/"); + command_bar->SetAutocompleteMatchesForTesting(ACMatches{history_match}); + + EXPECT_EQ(2, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"example.com")); +} + +IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, + KeepsAskAiSecondWhileReservingSearch) { + browser()->profile()->GetPrefs()->SetBoolean( + dao::prefs::kDaoEnhancedCommandBarSuggestionsEnabled, true); + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"dao browser", + u""); + + ACMatches matches; + for (int i = 0; i < 5; ++i) { + AutocompleteMatch match(nullptr, 1000 - i, false, + AutocompleteMatchType::HISTORY_URL); + match.allowed_to_be_default_match = i == 0; + match.fill_into_edit = + base::ASCIIToUTF16("history" + base::NumberToString(i)); + match.contents = match.fill_into_edit; + match.destination_url = + GURL("https://example.com/" + base::NumberToString(i)); + matches.push_back(std::move(match)); + } + command_bar->SetAutocompleteMatchesForTesting(matches); + + EXPECT_EQ(1, command_bar->GetAskAiRowIndexForTesting()); + EXPECT_EQ(5, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"Ask AI: dao browser")); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"dao browser")); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"Search")); + EXPECT_FALSE(HasDescendantLabelText(command_bar, u"history3")); +} + +IN_PROC_BROWSER_TEST_F( + DaoCommandBarBrowserTest, + ExactSearchTabMatchDoesNotReplaceReservedSearchAction) { + embedded_test_server()->RegisterRequestHandler(base::BindRepeating( + [](const net::test_server::HttpRequest& request) + -> std::unique_ptr { + if (!base::StartsWith(request.relative_url, "/search", + base::CompareCase::SENSITIVE)) { + return nullptr; + } + auto response = std::make_unique(); + response->set_code(net::HTTP_OK); + response->set_content_type("text/html"); + response->set_content("results"); + return response; + })); + ASSERT_TRUE(embedded_test_server()->Start()); + + TemplateURLService* template_url_service = + TemplateURLServiceFactory::GetForProfile(browser()->profile()); + ASSERT_NE(nullptr, template_url_service); + search_test_utils::WaitForTemplateURLServiceToLoad(template_url_service); + TemplateURLData data; + data.SetShortName(u"Dao Test Search"); + data.SetKeyword(u"dao-test"); + data.SetURL(embedded_test_server()->GetURL("/search?q={searchTerms}").spec()); + TemplateURL* template_url = + template_url_service->Add(std::make_unique(data)); + ASSERT_NE(nullptr, template_url); + template_url_service->SetUserSelectedDefaultSearchProvider(template_url); + + browser()->profile()->GetPrefs()->SetBoolean(dao::prefs::kDaoAskAiEnabled, + false); + browser()->profile()->GetPrefs()->SetBoolean( + dao::prefs::kDaoEnhancedCommandBarSuggestionsEnabled, true); + DaoCommandBarView* command_bar = GetBrowserView(browser())->dao_command_bar(); + ASSERT_NE(nullptr, command_bar); + command_bar->ShowForNewTab(); + command_bar->SetUserInputAndInlineAutocompletionForTesting(u"example.com", + u""); + + AutocompleteMatch tab_search_match( + nullptr, 1000, false, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); + tab_search_match.allowed_to_be_default_match = true; + tab_search_match.fill_into_edit = u"example.com"; + tab_search_match.contents = u"example.com"; + tab_search_match.contents_class = { + {0, AutocompleteMatch::ACMatchClassification::NONE}}; + tab_search_match.destination_url = + embedded_test_server()->GetURL("/search?q=example.com"); + tab_search_match.has_tab_match = true; + + ACMatches matches{tab_search_match}; + for (int i = 0; i < 4; ++i) { + AutocompleteMatch match(nullptr, 1000 - i, false, + AutocompleteMatchType::HISTORY_URL); + match.fill_into_edit = + base::ASCIIToUTF16("history" + base::NumberToString(i)); + match.contents = match.fill_into_edit; + match.contents_class = {{0, AutocompleteMatch::ACMatchClassification::URL}}; + match.destination_url = embedded_test_server()->GetURL( + "/title1.html?match=" + base::NumberToString(i)); + matches.push_back(std::move(match)); + } + command_bar->SetAutocompleteMatchesForTesting(matches); + + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"Search")); + + for (int i = 0; i < 4; ++i) { + SendDialogKey(GetBrowserView(browser())->GetWidget(), ui::VKEY_DOWN); + } + ASSERT_EQ(4, command_bar->GetSelectedIndexForTesting()); + + ui_test_utils::TabAddedWaiter tab_waiter(browser()); + SendDialogKey(GetBrowserView(browser())->GetWidget(), ui::VKEY_RETURN); + content::WebContents* contents = tab_waiter.Wait(); + ASSERT_NE(nullptr, contents); + ASSERT_TRUE(content::WaitForLoadStop(contents)); + + EXPECT_EQ(embedded_test_server()->GetURL("/search?q=example.com"), + contents->GetLastCommittedURL()); +} + IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, EnterSubmitsVisibleInlineAutocompletion) { ASSERT_TRUE(embedded_test_server()->Start()); @@ -3410,8 +3746,8 @@ IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, ASSERT_NE(nullptr, command_bar); command_bar->ShowForNewTab(); - // Simulate the state after the user deletes all input: the textfield is - // empty, but zero-prefix results still arrive and auto-select a row. + // Simulate a stale zero-prefix result arriving after the user deletes all + // input. The Command Bar must discard it and keep the blank state empty. command_bar->SetUserInputAndInlineAutocompletionForTesting(u"", u""); AutocompleteMatch zero_prefix_match(nullptr, 1000, false, @@ -3424,13 +3760,14 @@ IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, zero_prefix_match.destination_url = GURL("https://github.com/"); command_bar->SetAutocompleteMatchesForTesting(ACMatches{zero_prefix_match}); - ASSERT_EQ(0, command_bar->GetSelectedIndexForTesting()); + ASSERT_EQ(0, command_bar->GetVisibleSuggestionCountForTesting()); + ASSERT_EQ(-1, command_bar->GetSelectedIndexForTesting()); const int tab_count = browser()->tab_strip_model()->count(); SendDialogKey(GetBrowserView(browser())->GetWidget(), ui::VKEY_RETURN); // Enter on empty input must only dismiss the bar — never navigate to the - // auto-selected zero-prefix suggestion. + // stale zero-prefix suggestion. EXPECT_EQ(tab_count, browser()->tab_strip_model()->count()); EXPECT_FALSE(command_bar->GetVisible()); } @@ -3512,7 +3849,8 @@ IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, EXPECT_EQ(1, command_bar->GetAskAiRowIndexForTesting()); EXPECT_EQ(0, command_bar->GetSelectedIndexForTesting()); EXPECT_TRUE(HasDescendantLabelText(command_bar, u"Ask AI: github")); - EXPECT_EQ(2, command_bar->GetVisibleSuggestionCountForTesting()); + EXPECT_TRUE(HasDescendantLabelText(command_bar, u"github")); + EXPECT_EQ(3, command_bar->GetVisibleSuggestionCountForTesting()); } IN_PROC_BROWSER_TEST_F(DaoCommandBarBrowserTest, diff --git a/src/dao/browser/ui/views/dao_command_bar_view.cc b/src/dao/browser/ui/views/dao_command_bar_view.cc index a3ec9b8..4908ec8 100644 --- a/src/dao/browser/ui/views/dao_command_bar_view.cc +++ b/src/dao/browser/ui/views/dao_command_bar_view.cc @@ -4,8 +4,11 @@ #include "dao/browser/ui/views/dao_command_bar_view.h" +#include + #include "base/strings/utf_string_conversions.h" #include "base/strings/escape.h" +#include "base/strings/string_util.h" #include "base/memory/scoped_refptr.h" #include "base/task/single_thread_task_runner.h" #include "components/omnibox/browser/autocomplete_classifier.h" @@ -80,6 +83,10 @@ bool LooksLikeLocalFilePath(const std::string& text) { return !text.empty() && (text[0] == '/' || text[0] == '~'); } +std::u16string NormalizeSearchTerms(const std::u16string& text) { + return std::u16string(base::TrimWhitespace(text, base::TRIM_ALL)); +} + } // namespace // Custom Textfield that prevents FocusManager from intercepting Tab for focus @@ -477,17 +484,13 @@ void DaoCommandBarView::Show() { textfield_->SetText(u""); user_input_text_.clear(); UpdateInputIcon(); - if (EnhancedSuggestionsEnabled()) { - StartAutocomplete(u""); - } + ClearSuggestions(); } } else { textfield_->SetText(u""); user_input_text_.clear(); UpdateInputIcon(); - if (EnhancedSuggestionsEnabled()) { - StartAutocomplete(u""); - } + ClearSuggestions(); } // Defer focus request to avoid being overridden by Chromium's focus @@ -554,9 +557,7 @@ void DaoCommandBarView::ShowForNewTab() { // Show search icon for new tab mode (empty input) UpdateInputIcon(); - if (EnhancedSuggestionsEnabled()) { - StartAutocomplete(u""); - } + ClearSuggestions(); base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( FROM_HERE, base::BindOnce(&DaoCommandBarView::DeferredRequestFocus, @@ -575,8 +576,7 @@ void DaoCommandBarView::Hide() { } StopAutocomplete(); - dropdown_container_->SetVisible(false); - visible_suggestion_count_ = 0; + ClearSuggestions(); SetVisible(false); @@ -738,15 +738,9 @@ void DaoCommandBarView::ContentsChanged(views::Textfield* sender, UpdateInputIcon(); - if (new_contents.empty()) { - if (EnhancedSuggestionsEnabled()) { - StartAutocomplete(new_contents); - return; - } + if (NormalizeSearchTerms(new_contents).empty()) { StopAutocomplete(); - dropdown_container_->SetVisible(false); - visible_suggestion_count_ = 0; - InvalidateLayout(); + ClearSuggestions(); return; } @@ -910,29 +904,73 @@ void DaoCommandBarView::StopAutocomplete() { selection_explicitly_changed_ = false; } +void DaoCommandBarView::ClearSuggestions() { + for (DaoSuggestionItemView* suggestion_view : suggestion_views_) { + suggestion_view->SetVisible(false); + suggestion_view->SetSelected(false); + } + visible_matches_.clear(); + dropdown_container_->SetVisible(false); + visible_suggestion_count_ = 0; + selected_index_ = -1; + selection_explicitly_changed_ = false; + ask_ai_row_index_ = -1; + InvalidateLayout(); +} + void DaoCommandBarView::UpdateSuggestions() { if (!autocomplete_controller_) { return; } + const std::u16string search_terms = NormalizeSearchTerms(user_input_text_); + if (search_terms.empty()) { + ClearSuggestions(); + return; + } + const bool enhanced_suggestions_enabled = EnhancedSuggestionsEnabled(); const AutocompleteResult& result = autocomplete_controller_->result(); - int match_count = std::min(static_cast(result.size()), kMaxSuggestions); - - // Keep Ask AI in the same slot across default and enhanced modes: after the - // top autocomplete match when one exists, otherwise as the first row. - // When real matches already fill all kMaxSuggestions slots, the last one - // is evicted to make room — it tends to be the lowest-relevance entry. const bool show_ask_ai = ShouldShowAskAiSuggestion(); - ask_ai_row_index_ = -1; - int match_slots = match_count; - if (show_ask_ai) { - ask_ai_row_index_ = std::min(1, match_count); - if (match_slots + 1 > kMaxSuggestions) { - match_slots = kMaxSuggestions - 1; + const int max_match_slots = kMaxSuggestions - (show_ask_ai ? 1 : 0); + + visible_matches_.clear(); + for (size_t i = 0; + i < result.size() && + visible_matches_.size() < static_cast(max_match_slots); + ++i) { + visible_matches_.push_back(result.match_at(i)); + } + + const AutocompleteMatch* exact_search_match = nullptr; + for (size_t i = 0; i < result.size(); ++i) { + if (IsExactSearchMatch(result.match_at(i), search_terms)) { + exact_search_match = &result.match_at(i); + break; + } + } + + const bool exact_search_is_visible = + std::any_of(visible_matches_.begin(), visible_matches_.end(), + [&](const AutocompleteMatch& match) { + return IsExactSearchMatch(match, search_terms); + }); + if (!exact_search_is_visible) { + AutocompleteMatch reserved_match = + exact_search_match ? *exact_search_match + : CreateExactSearchMatch(search_terms); + if (visible_matches_.size() < static_cast(max_match_slots)) { + visible_matches_.push_back(std::move(reserved_match)); + } else { + visible_matches_.back() = std::move(reserved_match); } } + // Keep Ask AI in the same slot across default and enhanced modes: after the + // top autocomplete match when one exists, otherwise as the first row. + ask_ai_row_index_ = + show_ask_ai ? std::min(1, static_cast(visible_matches_.size())) : -1; + // Check if we have a bookmark model for icon determination bookmarks::BookmarkModel* bookmark_model = BookmarkModelFactory::GetForBrowserContext(browser_->profile()); @@ -955,15 +993,14 @@ void DaoCommandBarView::UpdateSuggestions() { : std::u16string()); suggestion_views_[i]->SetVisible(true); suggestion_views_[i]->SetSelected(i == selected_index_); - } else if (match_index < match_slots) { - const AutocompleteMatch& match = result.match_at(match_index); + } else if (match_index < static_cast(visible_matches_.size())) { + const AutocompleteMatch& match = visible_matches_[match_index]; bool is_bookmark = - bookmark_model && - bookmark_model->IsBookmarked(match.destination_url); - suggestion_views_[i]->SetMatch( - match, is_bookmark, - enhanced_suggestions_enabled ? GetIntentLabelForMatch(match) - : std::u16string()); + bookmark_model && bookmark_model->IsBookmarked(match.destination_url); + suggestion_views_[i]->SetMatch(match, is_bookmark, + enhanced_suggestions_enabled + ? GetIntentLabelForMatch(match) + : std::u16string()); suggestion_views_[i]->SetVisible(true); suggestion_views_[i]->SetSelected(i == selected_index_); } else { @@ -972,8 +1009,8 @@ void DaoCommandBarView::UpdateSuggestions() { } } - visible_suggestion_count_ = - match_slots + (ask_ai_row_index_ >= 0 ? 1 : 0); + visible_suggestion_count_ = static_cast(visible_matches_.size()) + + (ask_ai_row_index_ >= 0 ? 1 : 0); if (visible_suggestion_count_ > 0) { dropdown_container_->SetVisible(true); @@ -1005,6 +1042,53 @@ std::u16string DaoCommandBarView::GetIntentLabelForMatch( return l10n_util::GetStringUTF16(IDS_DAO_SUGGESTION_INTENT_OPEN); } +GURL DaoCommandBarView::GetSearchUrl(const std::u16string& search_terms) const { + const std::u16string normalized_terms = NormalizeSearchTerms(search_terms); + if (normalized_terms.empty()) { + return GURL(); + } + + TemplateURLService* template_url_service = + TemplateURLServiceFactory::GetForProfile(browser_->profile()); + const TemplateURL* default_provider = + template_url_service ? template_url_service->GetDefaultSearchProvider() + : nullptr; + if (default_provider && default_provider->SupportsReplacement( + template_url_service->search_terms_data())) { + GURL url = default_provider->GenerateSearchURL( + template_url_service->search_terms_data(), normalized_terms); + if (url.is_valid()) { + return url; + } + } + + return GURL( + "https://www.google.com/search?q=" + + base::EscapeQueryParamValue(base::UTF16ToUTF8(normalized_terms), true)); +} + +AutocompleteMatch DaoCommandBarView::CreateExactSearchMatch( + const std::u16string& search_terms) const { + const std::u16string normalized_terms = NormalizeSearchTerms(search_terms); + AutocompleteMatch match(nullptr, 0, false, + AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED); + match.fill_into_edit = normalized_terms; + match.contents = normalized_terms; + match.contents_class = {{0, AutocompleteMatch::ACMatchClassification::NONE}}; + match.destination_url = GetSearchUrl(normalized_terms); + return match; +} + +bool DaoCommandBarView::IsExactSearchMatch( + const AutocompleteMatch& match, + const std::u16string& search_terms) const { + return AutocompleteMatch::IsSearchType(match.type) && + !match.has_tab_match.value_or(false) && + match.destination_url.is_valid() && + NormalizeSearchTerms(match.fill_into_edit) == + NormalizeSearchTerms(search_terms); +} + std::u16string DaoCommandBarView::GetInlineAutocompletionForResult() const { if (!autocomplete_controller_ || suppress_ghost_for_current_query_ || user_input_text_.empty()) { @@ -1086,19 +1170,18 @@ DaoCommandBarView::GetVisibleInlineAutocompletionMatch() const { const AutocompleteMatch* DaoCommandBarView::GetSelectedVisibleAutocompleteMatch() const { - if (!autocomplete_controller_ || selected_index_ < 0 || - selected_index_ == ask_ai_row_index_) { + if (selected_index_ < 0 || selected_index_ == ask_ai_row_index_) { return nullptr; } - const AutocompleteResult& result = autocomplete_controller_->result(); int match_index = selected_index_; if (ask_ai_row_index_ >= 0 && selected_index_ > ask_ai_row_index_) { match_index = selected_index_ - 1; } - if (match_index >= 0 && match_index < static_cast(result.size())) { - return &result.match_at(match_index); + if (match_index >= 0 && + match_index < static_cast(visible_matches_.size())) { + return &visible_matches_[match_index]; } return nullptr; @@ -1193,43 +1276,37 @@ void DaoCommandBarView::UpdateInputIcon() { } // If there's a selected autocomplete match, use its type. - // Shift past the Ask-AI slot when one is inserted before the selection. - if (autocomplete_controller_ && selected_index_ >= 0) { - const AutocompleteResult& result = autocomplete_controller_->result(); - int match_index = selected_index_; - if (ask_ai_row_index_ >= 0 && selected_index_ > ask_ai_row_index_) { - match_index = selected_index_ - 1; - } - if (match_index < static_cast(result.size())) { - const AutocompleteMatch& match = result.match_at(match_index); - bool is_search = AutocompleteMatch::IsSearchType(match.type); - if (is_search) { - favicon_icon_->SetImage(ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon( - vector_icons::kSearchChromeRefreshIcon, 18, icon_color))); - } else { - // Set page icon as immediate fallback, then try loading favicon - favicon_icon_->SetImage(ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon( - omnibox::kPageChromeRefreshIcon, 18, icon_color))); - - if (match.destination_url.is_valid() && - match.destination_url.SchemeIsHTTPOrHTTPS()) { - favicon::FaviconService* favicon_service = - FaviconServiceFactory::GetForProfile( - browser_->profile(), ServiceAccessType::EXPLICIT_ACCESS); - if (favicon_service) { - pending_icon_favicon_url_ = match.destination_url; - favicon_service->GetFaviconImageForPageURL( - match.destination_url, - base::BindOnce(&DaoCommandBarView::OnInputFaviconFetched, - base::Unretained(this), - match.destination_url), - &icon_favicon_tracker_); - } + if (const AutocompleteMatch* selected_match = + GetSelectedVisibleAutocompleteMatch()) { + const AutocompleteMatch& match = *selected_match; + bool is_search = AutocompleteMatch::IsSearchType(match.type); + if (is_search) { + favicon_icon_->SetImage( + ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon( + vector_icons::kSearchChromeRefreshIcon, 18, icon_color))); + } else { + // Set page icon as immediate fallback, then try loading favicon + favicon_icon_->SetImage( + ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon( + omnibox::kPageChromeRefreshIcon, 18, icon_color))); + + if (match.destination_url.is_valid() && + match.destination_url.SchemeIsHTTPOrHTTPS()) { + favicon::FaviconService* favicon_service = + FaviconServiceFactory::GetForProfile( + browser_->profile(), ServiceAccessType::EXPLICIT_ACCESS); + if (favicon_service) { + pending_icon_favicon_url_ = match.destination_url; + favicon_service->GetFaviconImageForPageURL( + match.destination_url, + base::BindOnce(&DaoCommandBarView::OnInputFaviconFetched, + base::Unretained(this), match.destination_url), + &icon_favicon_tracker_); } } - favicon_icon_->SetVisible(true); - return; } + favicon_icon_->SetVisible(true); + return; } // Fallback: determine icon from input text @@ -1275,6 +1352,7 @@ void DaoCommandBarView::SetUserInputAndInlineAutocompletionForTesting( selected_index_ = -1; selection_explicitly_changed_ = false; ask_ai_row_index_ = -1; + visible_matches_.clear(); updating_textfield_ = true; textfield_->SetText(user_input); @@ -1352,10 +1430,7 @@ void DaoCommandBarView::SetSelectedIndex(int index, bool user_initiated) { } void DaoCommandBarView::ApplySelectedSuggestion() { - // Empty input: Enter only dismisses the bar. Enhanced mode still runs - // zero-prefix autocomplete on empty text and auto-selects the first row, - // so without this guard Enter would navigate to a suggestion the user - // never typed or picked. + // Empty input: Enter only dismisses the bar. if (user_input_text_.empty()) { Navigate(std::u16string()); return; @@ -1405,22 +1480,9 @@ void DaoCommandBarView::ApplySelectedSuggestion() { return; } - if (!autocomplete_controller_) { - Navigate(GetInlineAutocompletedInputText()); - return; - } - - const AutocompleteResult& result = autocomplete_controller_->result(); - - // Shift past the Ask-AI slot when one is inserted before the selection. - int match_index = selected_index_; - if (ask_ai_row_index_ >= 0 && selected_index_ > ask_ai_row_index_) { - match_index = selected_index_ - 1; - } - - if (match_index >= 0 && - match_index < static_cast(result.size())) { - NavigateToMatch(result.match_at(match_index)); + if (const AutocompleteMatch* selected_match = + GetSelectedVisibleAutocompleteMatch()) { + NavigateToMatch(*selected_match); } else { // No selected match — use plain text navigation Navigate(GetInlineAutocompletedInputText()); @@ -1569,22 +1631,7 @@ void DaoCommandBarView::Navigate(const std::u16string& text) { } url = GURL(input); } else { - TemplateURLService* template_url_service = - TemplateURLServiceFactory::GetForProfile(browser_->profile()); - const TemplateURL* default_provider = - template_url_service - ? template_url_service->GetDefaultSearchProvider() - : nullptr; - if (default_provider && - default_provider->SupportsReplacement( - template_url_service->search_terms_data())) { - url = default_provider->GenerateSearchURL( - template_url_service->search_terms_data(), text); - } - if (!url.is_valid()) { - url = GURL("https://www.google.com/search?q=" + - base::EscapeQueryParamValue(input, true)); - } + url = GetSearchUrl(text); } if (!url.is_valid()) { diff --git a/src/dao/browser/ui/views/dao_command_bar_view.h b/src/dao/browser/ui/views/dao_command_bar_view.h index bd20baa..dfa25b9 100644 --- a/src/dao/browser/ui/views/dao_command_bar_view.h +++ b/src/dao/browser/ui/views/dao_command_bar_view.h @@ -17,6 +17,7 @@ #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h" #include "components/favicon_base/favicon_types.h" #include "components/omnibox/browser/autocomplete_controller.h" +#include "components/omnibox/browser/autocomplete_match.h" #include "ui/native_theme/native_theme.h" #include "ui/native_theme/native_theme_observer.h" #include "ui/views/controls/textfield/textfield_controller.h" @@ -127,6 +128,7 @@ class DaoCommandBarView : public views::View, void InitAutocompleteController(); void StartAutocomplete(const std::u16string& text); void StopAutocomplete(); + void ClearSuggestions(); void UpdateSuggestions(); void UpdateGhostText(); void PositionGhostText(); @@ -153,6 +155,11 @@ class DaoCommandBarView : public views::View, int GetAutocompleteProviderTypesForCurrentMode() const; const AutocompleteMatch* GetSelectedVisibleAutocompleteMatch() const; std::u16string GetIntentLabelForMatch(const AutocompleteMatch& match) const; + GURL GetSearchUrl(const std::u16string& search_terms) const; + AutocompleteMatch CreateExactSearchMatch( + const std::u16string& search_terms) const; + bool IsExactSearchMatch(const AutocompleteMatch& match, + const std::u16string& search_terms) const; raw_ptr browser_; raw_ptr shadow_view_ = nullptr; @@ -164,6 +171,7 @@ class DaoCommandBarView : public views::View, raw_ptr dropdown_container_ = nullptr; std::vector> suggestion_views_; + std::vector visible_matches_; std::unique_ptr autocomplete_controller_; std::unique_ptr scheme_classifier_;