Ariver
2026-06-17 aba608b433a9dd7e218239947c2c983741fc7fc7
Prepare 7.9.2 build 20260617.2028
6 files modified
1 files added
328 ■■■■■ changed files
src/Apptag/ApptagApp.swift 48 ●●●●● patch | view | raw | blame | history
src/Apptag/ContentView.swift 34 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/CHANGELOG.md 11 ●●●●● patch | view | raw | blame | history
src/Scripts/macos14_availability_typecheck_qa.sh 78 ●●●●● patch | view | raw | blame | history
src/Scripts/tag_navigation_hover_scroll_qa.sh 74 ●●●●● patch | view | raw | blame | history
src/build.sh 79 ●●●●● patch | view | raw | blame | history
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] : []
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 }
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>
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 左侧标签栏和主体区域可能不显示该标签的问题;空标签现在会按当前视图显示最小容器,避免用户以为创建失败
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 \
src/Scripts/tag_navigation_hover_scroll_qa.sh
New file
@@ -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
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 \