From 5613caa749712d4ffc1daaaf4e5a314b58e12ffd Mon Sep 17 00:00:00 2001 From: abue-ammar Date: Thu, 24 Sep 2026 00:50:37 +0600 Subject: [PATCH] Keep AI commands out of Suggestions Suggestions ordered what the user opens by frecency alone, so a burst of AI Chat use put it above every app, and Quick AI led the built-in fill for a user with no history. AI is the lowest priority, so AppIndex.suggestions now drops the AI pane's commands (settingsOwner == .ai) along with meetings and Tinycast itself, and Quick AI leaves CommandID.suggestionPriority, where its value was dead. Both stay reachable by search, by their boosted terms, and in the Commands section of the empty list. --- Tests/fuzz-test.swift | 10 +++++----- Tinycast/Features/Launcher/Model/CommandID.swift | 1 - Tinycast/Features/Launcher/Service/AppIndex.swift | 5 +++-- docs/features/launcher.md | 5 +++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Tests/fuzz-test.swift b/Tests/fuzz-test.swift index 6ae1b2c0b..1c0959c54 100644 --- a/Tests/fuzz-test.swift +++ b/Tests/fuzz-test.swift @@ -387,20 +387,20 @@ struct FuzzTest { } let commands = [ - Candidate(name: "Clipboard History", priority: 80), Candidate(name: "AI Chat", priority: 90), - Candidate(name: "Search Files", priority: 70), Candidate(name: "My Schedule", priority: 60), + Candidate(name: "Search Files", priority: 70), Candidate(name: "Clipboard History", priority: 80), + Candidate(name: "My Schedule", priority: 60), Candidate(name: "Search Emoji & Symbols", priority: 50), Candidate(name: "Create Snippet", priority: 30) ] check( "a new user gets the built-ins, highest priority first", select(commands) == [ - "AI Chat", "Clipboard History", "Search Files", "My Schedule", "Search Emoji & Symbols" + "Clipboard History", "Search Files", "My Schedule", "Search Emoji & Symbols", "Create Snippet" ]) let used = [Candidate(name: "Safari", frecency: 40), Candidate(name: "Slack", frecency: 300)] check( "what the user opens comes first, most frecent first", - select(used + commands).prefix(3) == ["Slack", "Safari", "AI Chat"]) + select(used + commands).prefix(3) == ["Slack", "Safari", "Clipboard History"]) let many = (1...8).map { Candidate(name: "App \($0)", frecency: Double(100 + $0)) } check("never more than five", select(many + commands).count == LauncherSuggestions.limit) check( @@ -408,7 +408,7 @@ struct FuzzTest { !select([Candidate(name: "Slack", frecency: 300, hotKey: true)] + commands).contains("Slack")) check( "the fill skips a built-in the user already aliased", - !select([Candidate(name: "AI Chat", alias: "ai", priority: 90)]).contains("AI Chat")) + select([Candidate(name: "Clipboard History", alias: "cb", priority: 80)]).isEmpty) let fresh = [ Candidate(name: "New One", installedMinutesAgo: 1), Candidate(name: "New Two", installedMinutesAgo: 2), diff --git a/Tinycast/Features/Launcher/Model/CommandID.swift b/Tinycast/Features/Launcher/Model/CommandID.swift index d7dbc53d9..337c56df4 100644 --- a/Tinycast/Features/Launcher/Model/CommandID.swift +++ b/Tinycast/Features/Launcher/Model/CommandID.swift @@ -166,7 +166,6 @@ enum CommandID: String, CaseIterable, Sendable { /// Suggested, highest first, until the user's own habits fill the section. var suggestionPriority: Int? { switch self { - case .quickAI: 90 case .clipboardHistory: 80 case .searchFiles: 70 case .mySchedule: 60 diff --git a/Tinycast/Features/Launcher/Service/AppIndex.swift b/Tinycast/Features/Launcher/Service/AppIndex.swift index 2f1dfeecd..32ee7d60e 100644 --- a/Tinycast/Features/Launcher/Service/AppIndex.swift +++ b/Tinycast/Features/Launcher/Service/AppIndex.swift @@ -714,12 +714,13 @@ final class AppIndex { return ordered } - /// Meetings keep their own card, and Tinycast opening Tinycast goes nowhere. + /// Meetings keep their own card, AI is never pushed, and Tinycast opening Tinycast goes nowhere. private func suggestions( from entries: [AppEntry], usage: LauncherRankingStore.Snapshot, hotKeys: HotKeyManager ) -> [AppEntry] { let eligible = entries.filter { - $0.kind != .meeting && !($0.bundleID?.hasPrefix(Self.ownBundlePrefix) ?? false) + $0.kind != .meeting && $0.settingsOwner != .ai + && !($0.bundleID?.hasPrefix(Self.ownBundlePrefix) ?? false) } return LauncherSuggestions.select(from: eligible, now: usage.now) { entry in // `hotKeyAction` is nil for an extension command, whose shortcut is keyed by entry ID. diff --git a/docs/features/launcher.md b/docs/features/launcher.md index 09916cc5d..d012b25a9 100644 --- a/docs/features/launcher.md +++ b/docs/features/launcher.md @@ -412,14 +412,15 @@ so the sectioned view stays 1:1 with the flat selection. ### Suggestions `LauncherSuggestions.select` chooses at most five from every visible entry that is not a favorite, a -meeting or Tinycast itself: +meeting, an AI command or Tinycast itself. AI is the lowest priority, so Quick AI and AI Chat are +never suggested, however often they are opened: 1. up to two apps or extensions installed in the last five minutes and never opened — `AppEntry.installedAt` is the bundle's added-to-directory date; 2. entries with a score above 1 and no bound shortcut, in empty-list order — a shortcut is already the faster way in; 3. while fewer than five, built-in commands with no alias or shortcut, by - `CommandID.suggestionPriority`: AI Chat, Clipboard History, Search Files, My Schedule, Search Emoji & + `CommandID.suggestionPriority`: Clipboard History, Search Files, My Schedule, Search Emoji & Symbols, then Create Quicklink and Create Snippet. A command whose feature is off is absent from the index, so it is never offered.