From 1d19c0593ebe6abe60468e12e87459ab68586603 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 04 Jun 2026 12:07:09 +0800
Subject: [PATCH] Refresh app index when application folders change

---
 TODO.md                    |    6 ++
 Scripts/window_logic_qa.sh |   87 +++++++++++++++++++++--------
 Apptag/ContentView.swift   |   37 +++++++++---
 Apptag/DataLayer.swift     |   10 +++
 Apptag/Info.plist          |    4 
 CHANGELOG.md               |    8 ++
 6 files changed, 116 insertions(+), 36 deletions(-)

diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 86e511f..59964a3 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -376,6 +376,7 @@
     @State private var refreshInProgress = false
     @State private var refreshAgainAfterCurrent = false
     @State private var refreshAgainForceLayout = false
+    @State private var refreshAgainUseCache = true
     @FocusState private var bubbleNoteFocused: Bool
 
     // Quick Search
@@ -532,7 +533,7 @@
         }
         .onAppear {
             refreshNotchHeight()
-            refreshAppsIfNeeded()
+            refreshAppsForOverlay()
             if let initialQuickSearchSource, !initialQuickSearchConsumed {
                 initialQuickSearchConsumed = true
                 if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
@@ -545,6 +546,7 @@
                 }
                 quickSearchOpenedAt = Date()
                 refreshQuickSearchResults()
+                refreshAppsForQuickSearch()
                 NotificationCenter.default.post(
                     name: .tagLauncherQuickSearchVisibilityChanged,
                     object: nil,
@@ -558,7 +560,7 @@
         .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
             resetTransientDragState()
             refreshNotchHeight()
-            refreshAppsIfNeeded()
+            refreshAppsForOverlay()
         }
         .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
             resetTransientDragState()
@@ -676,7 +678,7 @@
                     selectedID: quickSearchSelectedID,
                     focusToken: quickSearchFocusToken,
                     selectionScrollToken: quickSearchSelectionScrollToken,
-                    isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
+                    isLoading: quickSearchShouldShowLoading,
                     maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
                     panelTopY: quickSearchPanelTopY(in: proxy.size),
                     panelHeight: quickSearchPanelContentHeight(in: proxy.size),
@@ -695,7 +697,7 @@
     }
 
     private func quickSearchPanelContentHeight(in size: CGSize) -> CGFloat {
-        let hasResultList = !(quickSearchDocuments.isEmpty && refreshInProgress)
+        let hasResultList = !quickSearchShouldShowLoading
             && quickSearchErrorMessage == nil
             && !quickSearchResults.isEmpty
         let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
@@ -735,7 +737,7 @@
         quickSearchErrorMessage = nil
         quickSearchFocusToken &+= 1
         refreshQuickSearchResults()
-        refreshApps()
+        refreshAppsForQuickSearch()
         NotificationCenter.default.post(
             name: .tagLauncherQuickSearchVisibilityChanged,
             object: nil,
@@ -752,6 +754,10 @@
             && smartStartNotice == nil
             && !appGridInteraction.dropRefreshVisible
             && !quickSearchVisible
+    }
+
+    private var quickSearchShouldShowLoading: Bool {
+        quickSearchVisible && refreshInProgress && quickSearchResults.isEmpty
     }
 
     private func closeQuickSearch(
@@ -1807,6 +1813,16 @@
         refreshApps()
     }
 
+    private func refreshAppsForOverlay() {
+        guard AppIndexer.shouldRefreshForSearchPathChanges() else { return }
+        refreshApps()
+    }
+
+    private func refreshAppsForQuickSearch() {
+        guard AppIndexer.shouldRefreshForSearchPathChanges() else { return }
+        refreshApps()
+    }
+
     private func refreshNotchHeight() {
         let mousePoint = NSEvent.mouseLocation
         let activeScreen = NSScreen.screens.first(where: {
@@ -2417,10 +2433,9 @@
 
     func refreshApps(forceLayoutRefresh: Bool = false, useCache: Bool = true) {
         guard !refreshInProgress else {
-            if forceLayoutRefresh {
-                refreshAgainAfterCurrent = true
-                refreshAgainForceLayout = true
-            }
+            refreshAgainAfterCurrent = true
+            refreshAgainForceLayout = refreshAgainForceLayout || forceLayoutRefresh
+            refreshAgainUseCache = refreshAgainUseCache && useCache
             return
         }
 
@@ -2436,9 +2451,11 @@
                 refreshInProgress = false
                 if refreshAgainAfterCurrent {
                     let shouldForceLayout = refreshAgainForceLayout
+                    let shouldUseCache = refreshAgainUseCache
                     refreshAgainAfterCurrent = false
                     refreshAgainForceLayout = false
-                    refreshApps(forceLayoutRefresh: shouldForceLayout)
+                    refreshAgainUseCache = true
+                    refreshApps(forceLayoutRefresh: shouldForceLayout, useCache: shouldUseCache)
                 }
             }
         }
diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index 060c58c..36fd8c8 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -145,6 +145,16 @@
         scanCacheLock.unlock()
     }
 
+    static func shouldRefreshForSearchPathChanges() -> Bool {
+        scanCacheLock.lock()
+        let hasCachedApps = cachedScanApps != nil
+        let cachedSignature = cachedSearchPathSignature
+        scanCacheLock.unlock()
+
+        guard hasCachedApps else { return true }
+        return cachedSignature != currentSearchPathSignature()
+    }
+
     private static func cachedScanIfFresh(matching searchPathSignature: [SearchPathSignature]) -> [AppInfo]? {
         scanCacheLock.lock()
         defer { scanCacheLock.unlock() }
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index 78ff12b..11f278e 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,9 +19,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>7.8.12</string>
+	<string>7.8.13</string>
 	<key>CFBundleVersion</key>
-	<string>20260604.0105</string>
+	<string>20260604.1121</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b4f277..e21d2ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # TagLauncher Changelog
 
+## [7.8.13] — 2026-06-04
+
+- 修复 App 运行期间新安装应用后,App Grid 和 Quick Search 仍复用旧应用索引、无法立即检索到新应用的问题
+- App Grid / Quick Search 打开时会轻量检查标准应用目录签名;签名未变化时不刷新也不重建界面,只有目录变化时才后台重扫
+- 刷新过程中再次打开 App Grid 或 Quick Search 会合并一个后续刷新请求,避免新安装发生在刷新窗口内被漏掉
+- 窗口 QA 对 Dock 点击打开、App Grid 内打开 Quick Search 和 Quick Search 结果坐标读取增加短重试,避免系统事件或窗口列表瞬时抖动导致误判
+- 版本号更新为 `7.8.13`,Build 更新为 `20260604.1121`
+
 ## [7.8.12] — 2026-06-04
 
 - 修复 Quick Search-only 模式刚打开后可能被过期鼠标关闭事件立即关掉的问题;现在 `Fn + Space` 打开后候选面板会保持稳定,键盘取消和再次按 `Fn + Space` 仍可立即关闭
diff --git a/Scripts/window_logic_qa.sh b/Scripts/window_logic_qa.sh
index 9e0c433..897add1 100755
--- a/Scripts/window_logic_qa.sh
+++ b/Scripts/window_logic_qa.sh
@@ -349,6 +349,20 @@
   click_xy "$x" "$y"
 }
 
+open_overlay_from_dock_with_retry() {
+  local output=""
+  for _ in {1..3}; do
+    click_taglauncher_dock_tile
+    sleep 0.8
+    if output="$(wait_swift_assert overlay 2>&1)"; then
+      printf '%s\n' "$output"
+      return 0
+    fi
+  done
+  printf '%s\n' "$output" >&2
+  return 1
+}
+
 assert_frontmost_taglauncher() {
   local frontmost
   frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')"
@@ -792,10 +806,10 @@
         return name.isEmpty
             && width >= 500
             && width <= 900
-            && height >= 120
+            && height >= 320
             && height <= 850
     }), let bounds = quickSearch[kCGWindowBounds as String] as? NSDictionary else {
-        fputs("FAIL: could not find quick search bounds\n", stderr)
+        fputs("FAIL: could not find quick search result-list bounds\n", stderr)
         exit(1)
     }
     let x = dimension(bounds, "X")
@@ -1029,16 +1043,37 @@
   return 1
 }
 
+open_appgrid_quick_search_with_retry() {
+  local output=""
+  for _ in {1..3}; do
+    send_keycode 49
+    sleep 0.4
+    if output="$(wait_swift_assert quick-search 2>&1)"; then
+      printf '%s\n' "$output"
+      return 0
+    fi
+  done
+  printf '%s\n' "$output" >&2
+  return 1
+}
+
 assert_fullscreen_overlay_stable() {
   local output=""
+  local consecutive_successes=0
   for _ in {1..20}; do
-    if ! output="$(swift "$assert_swift" fullscreen-overlay 2>&1)"; then
-      printf '%s\n' "$output" >&2
-      return 1
+    if output="$(swift "$assert_swift" fullscreen-overlay 2>&1)"; then
+      consecutive_successes=$((consecutive_successes + 1))
+      if [[ "$consecutive_successes" -ge 6 ]]; then
+        printf '%s\n' "$output"
+        return 0
+      fi
+    else
+      consecutive_successes=0
     fi
     sleep 0.1
   done
-  printf '%s\n' "$output"
+  printf '%s\n' "$output" >&2
+  return 1
 }
 
 show_overlay() {
@@ -1077,9 +1112,9 @@
 
 hover_quick_search_results() {
   local coords x y
-  coords="$(swift "$coords_swift" quick-search-result)"
+  coords="$(quick_search_result_coords_with_retry)"
   read -r x y <<<"$coords"
-  for offset in 0 42 84 126 84 42 0; do
+  for offset in 0 8 16 8 0; do
     move_xy "$x" "$((y + offset))"
     sleep 0.08
   done
@@ -1087,9 +1122,22 @@
 
 click_quick_search_result() {
   local coords x y
-  coords="$(swift "$coords_swift" quick-search-result)"
+  coords="$(quick_search_result_coords_with_retry)"
   read -r x y <<<"$coords"
   click_xy "$x" "$y"
+}
+
+quick_search_result_coords_with_retry() {
+  local output
+  for _ in {1..8}; do
+    if output="$(swift "$coords_swift" quick-search-result 2>&1)"; then
+      printf '%s\n' "$output"
+      return 0
+    fi
+    sleep 0.15
+  done
+  printf '%s\n' "$output" >&2
+  return 1
 }
 
 page_scroll_appgrid() {
@@ -1193,9 +1241,7 @@
 assert_single_dock_tile
 swift_assert no-overlay
 log "==> QA Dock reopen: showDockIcon=true opens App Grid from explicit app reopen"
-click_taglauncher_dock_tile
-sleep 1.0
-wait_swift_assert overlay
+open_overlay_from_dock_with_retry
 send_keycode 53
 sleep 0.4
 wait_swift_assert no-overlay
@@ -1254,9 +1300,7 @@
   wait_swift_assert fullscreen-overlay
   assert_fullscreen_overlay_stable
   log "==> QA fullscreen Space: quick search from appgrid does not switch Space"
-  send_keycode 49
-  sleep 0.4
-  wait_swift_assert quick-search
+  open_appgrid_quick_search_with_retry
   assert_fullscreen_overlay_stable
   send_keycode 53
   sleep 0.3
@@ -1288,8 +1332,7 @@
 swift_assert overlay
 
 log "==> QA 1/7 and 4/7: settings floats above appgrid and quick search"
-send_keycode 49
-sleep 0.4
+open_appgrid_quick_search_with_retry
 send_cmd_comma
 sleep 0.7
 swift_assert settings
@@ -1308,8 +1351,7 @@
 assert_frontmost_taglauncher
 
 log "==> QA 4/7 and 5/7: appgrid-space quick search and double-Esc behavior"
-send_keycode 49
-sleep 0.4
+open_appgrid_quick_search_with_retry
 send_keycode 53
 sleep 0.4
 swift_assert overlay
@@ -1321,9 +1363,7 @@
 show_overlay
 for _ in {1..5}; do
   page_scroll_appgrid
-  send_keycode 49
-  sleep 0.35
-  swift_assert quick-search
+  open_appgrid_quick_search_with_retry
   send_keycode 53
   sleep 0.25
   swift_assert overlay
@@ -1335,8 +1375,7 @@
 
 log "==> QA 5/7: clicking outside quick search closes search, not appgrid"
 show_overlay
-send_keycode 49
-sleep 0.4
+open_appgrid_quick_search_with_retry
 click_overlay_outside_quick_search
 sleep 0.4
 swift_assert overlay
diff --git a/TODO.md b/TODO.md
index 4e14362..1132d25 100644
--- a/TODO.md
+++ b/TODO.md
@@ -23,6 +23,12 @@
 
 ## Done
 
+- [2026-06-04] 修复新安装 App 后 App Grid / Quick Search 不能立即检索到的问题。
+  - 提交: 本次提交
+  - 结果: App Grid / Quick Search 打开时先轻量检查标准应用目录签名;签名未变化时不刷新也不重建界面,目录变化时才后台重扫应用索引。
+  - 范围: 不改搜索匹配规则、不改 App Grid 布局、不改窗口层级策略;只收敛应用索引刷新触发条件和 QA 脚本稳定性。
+  - 验证: 本地 `7.8.13 (20260604.1121)` targeted 输入 `notch` 命中 `TheBoringNotch` 且无“未找到”;`APP_BUILD=20260604.1121 zsh ./build.sh`、签名/版本校验、`Scripts/window_logic_qa.sh` 全部通过。
+
 - [2026-06-04] 修复 Quick Search 刚打开即被过期 dismiss 事件关闭的 QA 失败。
   - 提交: 本次提交
   - 结果: quick-search-only 模式会忽略打开初期的过期鼠标/背板关闭事件;键盘 Esc 和再次按 `Fn + Space` 仍立即关闭;QA 脚本对合成 `Fn + Space` 投递增加重试,并把结果点击坐标修正到首条结果行中心。

--
Gitblit v1.9.3