From aba608b433a9dd7e218239947c2c983741fc7fc7 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 17 Jun 2026 20:42:14 +0800
Subject: [PATCH] Prepare 7.9.2 build 20260617.2028

---
 src/build.sh                                     |   79 ++++++++++++
 src/CHANGELOG.md                                 |   11 +
 src/Apptag/Info.plist                            |    4 
 src/Apptag/ContentView.swift                     |   34 +++++
 src/Scripts/tag_navigation_hover_scroll_qa.sh    |   74 ++++++++++++
 src/Scripts/macos14_availability_typecheck_qa.sh |   78 ++++++++++++
 src/Apptag/ApptagApp.swift                       |   48 +++++++
 7 files changed, 318 insertions(+), 10 deletions(-)

diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index e4efd6d..f0df77a 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -342,11 +342,53 @@
         }
     }
 
+    private func foregroundWindowForActivationPolicyRestore() -> NSWindow? {
+        if let settingsWindow, settingsWindow.isVisible {
+            return settingsWindow
+        }
+        if let overlayWindow, overlayWindow.isVisible {
+            return overlayWindow
+        }
+        return NSApp.keyWindow
+    }
+
+    private func setLauncherActivationPolicy(_ desiredPolicy: NSApplication.ActivationPolicy) {
+        guard NSApp.activationPolicy() != desiredPolicy else { return }
+
+        let restoreWindow = desiredPolicy == .accessory && NSApp.isActive
+            ? foregroundWindowForActivationPolicyRestore()
+            : nil
+
+        // Deactivate before switching regular -> accessory; otherwise Dock can
+        // keep a stale running tile until Dock itself restarts.
+        if desiredPolicy == .accessory && NSApp.isActive {
+            NSApp.deactivate()
+        }
+
+        NSApp.setActivationPolicy(desiredPolicy)
+
+        guard desiredPolicy == .accessory,
+              let restoreWindow,
+              restoreWindow.isVisible
+        else { return }
+
+        DispatchQueue.main.async { [weak self, weak restoreWindow] in
+            guard let self,
+                  let restoreWindow,
+                  restoreWindow.isVisible,
+                  self.requiresForegroundOwnership
+            else { return }
+            NSApp.activate(ignoringOtherApps: true)
+            restoreWindow.makeKeyAndOrderFront(nil)
+            restoreWindow.orderFrontRegardless()
+        }
+    }
+
     private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         let desiredPolicy: NSApplication.ActivationPolicy = showDock ? .regular : .accessory
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
         if activate, showDock {
             claimLauncherForeground(keyWindow: keyWindow)
@@ -363,7 +405,7 @@
         overlayGeneration expectedOverlayGeneration: Int? = nil
     ) {
         if NSApp.activationPolicy() != .regular {
-            NSApp.setActivationPolicy(.regular)
+            setLauncherActivationPolicy(.regular)
         }
 
         if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
@@ -425,7 +467,7 @@
             ? .regular
             : (showDock ? .regular : .accessory))))
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
 
         let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index ed6f932..745a918 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -82,6 +82,10 @@
     }
 
     func updateNSView(_ container: TextFieldContainer, context: Context) {
+        if let editor = container.textField.currentEditor() as? NSTextView,
+           editor.hasMarkedText() {
+            return
+        }
         if container.textField.stringValue != text {
             container.textField.stringValue = text
         }
@@ -106,6 +110,9 @@
         }
 
         func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
+            if textView.hasMarkedText() {
+                return false
+            }
             if commandSelector == #selector(NSResponder.insertNewline(_:)) {
                 onSubmit?()
                 return true
@@ -319,6 +326,8 @@
     var bubbleDraftNote = ""
     var tagNavigationHoveredGroupName: String? = nil
     var tagNavigationAppDropTargetName: String? = nil
+    var tagNavigationLastHoverScrollID: String? = nil
+    var tagNavigationLastHoverScrollAt: Date? = nil
 }
 
 private struct EditActionFeedback: Identifiable {
@@ -436,6 +445,8 @@
             ?? (isColorlessContainerMode ? filledColorlessContainer : nil)
     }
     private let rightSidebarFloatingClearance: CGFloat = 44
+    private let tagNavigationHoverScrollDelay: TimeInterval = 0.14
+    private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
     private var floatingButtonSurfaceColor: Color {
         colorScheme == .dark
             ? Color.white.opacity(0.10)
@@ -2040,7 +2051,7 @@
     }
 
     private func handleTagNavigationHover(_ id: String, active: Bool) {
-        if appGridInteraction.appDragModeActive {
+        if appGridInteraction.appDragModeActive || tagNavDragModeActive {
             if !active, appGridInteraction.tagNavigationHoveredGroupName == id {
                 appGridInteraction.tagNavigationHoveredGroupName = nil
             }
@@ -2056,6 +2067,7 @@
 
         appGridInteraction.tagNavigationHoveredGroupName = id
         fillColorlessContainer(id)
+        scheduleTagNavigationHoverScroll(id)
     }
 
     private func handleTagNavigationAppDropHover(_ id: String, active: Bool) {
@@ -2076,6 +2088,26 @@
         appGridInteraction.tagNavigationAppDropTargetName = id
     }
 
+    private func scheduleTagNavigationHoverScroll(_ id: String) {
+        DispatchQueue.main.asyncAfter(deadline: .now() + tagNavigationHoverScrollDelay) {
+            guard appGridInteraction.tagNavigationHoveredGroupName == id else { return }
+            guard !appGridInteraction.appDragModeActive && !tagNavDragModeActive else { return }
+            scrollToTagFromHover(id)
+        }
+    }
+
+    private func scrollToTagFromHover(_ id: String) {
+        let now = Date()
+        if appGridInteraction.tagNavigationLastHoverScrollID == id,
+           let lastScrollAt = appGridInteraction.tagNavigationLastHoverScrollAt,
+           now.timeIntervalSince(lastScrollAt) < tagNavigationHoverScrollInterval {
+            return
+        }
+        appGridInteraction.tagNavigationLastHoverScrollID = id
+        appGridInteraction.tagNavigationLastHoverScrollAt = now
+        scrollTo(id)
+    }
+
     private func fillColorlessContainer(_ id: String) {
         guard isColorlessContainerMode else { return }
         guard filledColorlessContainer != id else { return }
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 4cf0480..8faf870 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,9 @@
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>7.9.1</string>
+	<string>7.9.2</string>
 	<key>CFBundleVersion</key>
-	<string>20260614.1454</string>
+	<string>20260617.2028</string>
 	<key>LSApplicationCategoryType</key>
 	<string>public.app-category.utilities</string>
 	<key>LSMinimumSystemVersion</key>
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 44d76aa..c8782d5 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,16 @@
 # TagLauncher Changelog
 
+## [7.9.2] — 2026-06-17
+
+- 恢复 App Grid 标签列表 hover 自动滚动:鼠标停到某个标签时会自动滚动到对应容器,点击标签仍保持立即滚动
+- 为 hover 自动滚动增加短延迟和同标签节流,避免快速扫过标签时重新引入容器抖动
+- 保留拖拽保护:拖动应用或拖动标签排序时,标签 hover 不会触发自动滚动,避免干扰打标签和排序操作
+- 修复标签输入偶发只能输入英文的问题;中文输入法组合字期间不再被 UI 状态刷新打断
+- 修复关闭“在 Dock 中显示”后 Dock 图标可能残留的问题,切换到菜单栏模式时会更稳定地刷新 macOS activation policy
+- 加固构建脚本和 macOS 14 typecheck QA 的 SDK 选择逻辑,避免本机 Swift / Xcode SDK 不匹配时无法生成包
+- 新增 `tag_navigation_hover_scroll_qa.sh`,把“hover 标签必须触发受控滚动”纳入回归门禁
+- 版本号更新为 `7.9.2`,Build 更新为 `20260617.2028`
+
 ## [7.9.1] — 2026-06-13
 
 - 修复新建空标签后 App Grid 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败
diff --git a/src/Scripts/macos14_availability_typecheck_qa.sh b/src/Scripts/macos14_availability_typecheck_qa.sh
index a756e9c..89d00b0 100755
--- a/src/Scripts/macos14_availability_typecheck_qa.sh
+++ b/src/Scripts/macos14_availability_typecheck_qa.sh
@@ -5,13 +5,14 @@
 SWIFT_DIR="$ROOT_DIR/Apptag"
 MACOS_DEPLOYMENT_TARGET="${MACOS_DEPLOYMENT_TARGET:-14.0}"
 TARGET_ARCHES_RAW="${TARGET_ARCHES:-arm64}"
+SWIFTC="${SWIFTC:-swiftc}"
 
 if [[ ! "$MACOS_DEPLOYMENT_TARGET" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
   echo "FAIL: invalid MACOS_DEPLOYMENT_TARGET: $MACOS_DEPLOYMENT_TARGET" >&2
   exit 1
 fi
 
-SDK_PATH="$(xcrun --sdk macosx --show-sdk-path)"
+SDK_PATH="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
 MODULE_CACHE_DIR="${MODULE_CACHE_DIR:-${TMPDIR:-/tmp}/taglauncher-macos14-typecheck-cache}"
 mkdir -p "$MODULE_CACHE_DIR"
 
@@ -30,6 +31,76 @@
   [[ -n "$arch" ]] && TARGET_ARCHES+=("$arch")
 done < <(tr ',' '\n' <<<"$TARGET_ARCHES_RAW" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
 
+select_macos_sdk() {
+  local target="$1"
+  local explicit_sdk="${SDK_PATH_OVERRIDE:-${SDKROOT:-}}"
+  if [[ -n "$explicit_sdk" ]]; then
+    if [[ ! -d "$explicit_sdk" ]]; then
+      echo "FAIL: explicit SDK path does not exist: $explicit_sdk" >&2
+      return 1
+    fi
+    echo "$explicit_sdk"
+    return 0
+  fi
+
+  local default_sdk="$SDK_PATH"
+  local sdk_candidates
+  sdk_candidates="$(python3 - "$default_sdk" <<'PY'
+import glob
+import os
+import re
+import sys
+
+seen = set()
+candidates = []
+
+def add(path):
+    if not path:
+        return
+    real = os.path.realpath(path)
+    if os.path.isdir(path) and real not in seen:
+        seen.add(real)
+        candidates.append(path)
+
+if len(sys.argv) > 1:
+    add(sys.argv[1])
+
+for path in glob.glob("/Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk"):
+    add(path)
+
+for path in glob.glob("/Applications/Xcode*.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk"):
+    add(path)
+
+def version_key(path):
+    name = os.path.basename(path)
+    match = re.search(r"MacOSX(\d+(?:\.\d+)*)\.sdk$", name)
+    if not match:
+        return ()
+    return tuple(int(part) for part in match.group(1).split("."))
+
+default_real = os.path.realpath(sys.argv[1]) if len(sys.argv) > 1 else ""
+rest = [p for p in candidates if os.path.realpath(p) != default_real]
+rest.sort(key=version_key, reverse=True)
+ordered = [p for p in candidates if os.path.realpath(p) == default_real] + rest
+for path in ordered:
+    print(path)
+PY
+)"
+
+  while IFS= read -r sdk; do
+    [[ -n "$sdk" ]] || continue
+    if printf 'import Swift\n' | "$SWIFTC" -typecheck -sdk "$sdk" -target "$target" - >/dev/null 2>&1; then
+      echo "$sdk"
+      return 0
+    fi
+  done <<< "$sdk_candidates"
+
+  echo "FAIL: no installed macOS SDK is compatible with current swiftc for target $target" >&2
+  echo "swiftc: $("$SWIFTC" --version | head -n 1)" >&2
+  echo "default SDK: $default_sdk" >&2
+  return 1
+}
+
 for arch in "${TARGET_ARCHES[@]}"; do
   case "$arch" in
     arm64|x86_64) ;;
@@ -40,8 +111,11 @@
   esac
 
   target="${arch}-apple-macosx${MACOS_DEPLOYMENT_TARGET}"
+  SDK_PATH="$(select_macos_sdk "$target")"
   echo "==> Typechecking for $target"
-  swiftc -typecheck \
+  echo "==> Using SDK: $SDK_PATH"
+  echo "==> Using Swift compiler: $SWIFTC"
+  "$SWIFTC" -typecheck \
     -framework AppKit \
     -framework SwiftUI \
     -framework Carbon \
diff --git a/src/Scripts/tag_navigation_hover_scroll_qa.sh b/src/Scripts/tag_navigation_hover_scroll_qa.sh
new file mode 100755
index 0000000..b275908
--- /dev/null
+++ b/src/Scripts/tag_navigation_hover_scroll_qa.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift"
+
+fail() {
+  printf 'FAIL: %s\n' "$*" >&2
+  exit 1
+}
+
+[[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift"
+
+python3 - "$CONTENT_VIEW_SWIFT" <<'PY'
+import pathlib
+import re
+import sys
+
+content = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
+
+
+def fail(message: str) -> None:
+    print(f"FAIL: {message}", file=sys.stderr)
+    sys.exit(1)
+
+
+def require(pattern: str, source: str, message: str) -> None:
+    if not re.search(pattern, source, re.S):
+        fail(message)
+
+
+require(
+    r"var\s+tagNavigationLastHoverScrollID\s*:\s*String\?\s*=\s*nil",
+    content,
+    "hover scroll must remember the last scrolled tag id",
+)
+require(
+    r"var\s+tagNavigationLastHoverScrollAt\s*:\s*Date\?\s*=\s*nil",
+    content,
+    "hover scroll must remember the last scroll time",
+)
+require(
+    r"private\s+let\s+tagNavigationHoverScrollDelay\s*:\s*TimeInterval\s*=\s*0\.14",
+    content,
+    "hover scroll must use the reviewed short hover-intent delay",
+)
+require(
+    r"private\s+let\s+tagNavigationHoverScrollInterval\s*:\s*TimeInterval\s*=\s*0\.22",
+    content,
+    "hover scroll must keep throttling repeated scrolls for the same tag",
+)
+require(
+    r"private\s+func\s+activateTagNavigation\(_\s+id:\s+String\)\s*\{(?P<body>.*?)scrollTo\(id\)(?P<body2>.*?)\n\s*\}",
+    content,
+    "clicking a tag must still scroll immediately",
+)
+require(
+    r"private\s+func\s+handleTagNavigationHover\(_\s+id:\s+String,\s+active:\s+Bool\)\s*\{(?P<body>.*?)appGridInteraction\.appDragModeActive\s*\|\|\s*tagNavDragModeActive(?P<body2>.*?)scheduleTagNavigationHoverScroll\(id\)",
+    content,
+    "hovering a tag must schedule controlled scrolling and must be disabled during drag states",
+)
+require(
+    r"private\s+func\s+scheduleTagNavigationHoverScroll\(_\s+id:\s+String\)\s*\{(?P<body>.*?)DispatchQueue\.main\.asyncAfter\(deadline:\s*\.now\(\)\s*\+\s*tagNavigationHoverScrollDelay\)(?P<body2>.*?)tagNavigationHoveredGroupName\s*==\s*id(?P<body3>.*?)!appGridInteraction\.appDragModeActive\s*&&\s*!tagNavDragModeActive(?P<body4>.*?)scrollToTagFromHover\(id\)",
+    content,
+    "scheduled hover scroll must only fire if the same tag is still hovered and no drag state is active",
+)
+require(
+    r"private\s+func\s+scrollToTagFromHover\(_\s+id:\s+String\)\s*\{(?P<body>.*?)tagNavigationLastHoverScrollID\s*==\s*id(?P<body2>.*?)timeIntervalSince\(lastScrollAt\)\s*<\s*tagNavigationHoverScrollInterval(?P<body3>.*?)scrollTo\(id\)",
+    content,
+    "hover scrolling must be throttled and eventually call scrollTo(id)",
+)
+
+print("PASS: tag navigation hover scroll semantics are guarded")
+PY
diff --git a/src/build.sh b/src/build.sh
index e91bfd2..7cd66d7 100644
--- a/src/build.sh
+++ b/src/build.sh
@@ -12,10 +12,83 @@
 INFO_PLIST="$SWIFT_DIR/Info.plist"
 MODULE_CACHE_DIR="${MODULE_CACHE_DIR:-$BUILD_DIR/ModuleCache}"
 
-SDK_PATH=$(xcrun --sdk macosx --show-sdk-path)
 MACOS_DEPLOYMENT_TARGET="${MACOS_DEPLOYMENT_TARGET:-14.0}"
 TARGET_ARCH="${TARGET_ARCH:-arm64}"
 TARGET="${TARGET_ARCH}-apple-macosx${MACOS_DEPLOYMENT_TARGET}"
+SWIFTC="${SWIFTC:-swiftc}"
+
+select_macos_sdk() {
+    local target="$1"
+    local explicit_sdk="${SDK_PATH:-${SDKROOT:-}}"
+    if [ -n "$explicit_sdk" ]; then
+        if [ ! -d "$explicit_sdk" ]; then
+            echo "❌ Explicit SDK path does not exist: $explicit_sdk" >&2
+            return 1
+        fi
+        echo "$explicit_sdk"
+        return 0
+    fi
+
+    local default_sdk
+    default_sdk="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
+    local sdk_candidates
+    sdk_candidates="$(python3 - "$default_sdk" <<'PY'
+import glob
+import os
+import re
+import sys
+
+seen = set()
+candidates = []
+
+def add(path):
+    if not path:
+        return
+    real = os.path.realpath(path)
+    if os.path.isdir(path) and real not in seen:
+        seen.add(real)
+        candidates.append(path)
+
+if len(sys.argv) > 1:
+    add(sys.argv[1])
+
+for path in glob.glob("/Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk"):
+    add(path)
+
+for path in glob.glob("/Applications/Xcode*.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX*.sdk"):
+    add(path)
+
+def version_key(path):
+    name = os.path.basename(path)
+    match = re.search(r"MacOSX(\d+(?:\.\d+)*)\.sdk$", name)
+    if not match:
+        return ()
+    return tuple(int(part) for part in match.group(1).split("."))
+
+default_real = os.path.realpath(sys.argv[1]) if len(sys.argv) > 1 else ""
+rest = [p for p in candidates if os.path.realpath(p) != default_real]
+rest.sort(key=version_key, reverse=True)
+ordered = [p for p in candidates if os.path.realpath(p) == default_real] + rest
+for path in ordered:
+    print(path)
+PY
+)"
+
+    while IFS= read -r sdk; do
+        [ -n "$sdk" ] || continue
+        if printf 'import Swift\n' | "$SWIFTC" -typecheck -sdk "$sdk" -target "$target" - >/dev/null 2>&1; then
+            echo "$sdk"
+            return 0
+        fi
+    done <<< "$sdk_candidates"
+
+    echo "❌ No installed macOS SDK is compatible with current swiftc for target $target" >&2
+    echo "   swiftc: $("$SWIFTC" --version | head -n 1)" >&2
+    echo "   default SDK: $default_sdk" >&2
+    return 1
+}
+
+SDK_PATH="$(select_macos_sdk "$TARGET")"
 
 APP_VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST")
 APP_BUILD="${APP_BUILD:-$(date '+%Y%m%d.%H%M')}"
@@ -26,6 +99,8 @@
 fi
 
 echo "==> Packaging $APP_NAME $APP_VERSION ($APP_BUILD)"
+echo "==> Using SDK: $SDK_PATH"
+echo "==> Using Swift compiler: $SWIFTC"
 
 # --- Optional: App Store / Sandbox signing ---
 # Set CODESIGN_IDENTITY to your "Apple Distribution" or "Mac Developer" cert name.
@@ -58,7 +133,7 @@
 while IFS= read -r file; do
     SWIFT_FILES+=("$file")
 done < <(find "$SWIFT_DIR" -name '*.swift' -print | sort)
-swiftc \
+"$SWIFTC" \
     -o "$MACOS_DIR/$APP_NAME" \
     -framework AppKit \
     -framework SwiftUI \

--
Gitblit v1.9.3