Skip to content

feat(log): differentiate activation reasons and add row context#13

Merged
BlackHole1 merged 2 commits into
mainfrom
improve-log
Jun 12, 2026
Merged

feat(log): differentiate activation reasons and add row context#13
BlackHole1 merged 2 commits into
mainfrom
improve-log

Conversation

@BlackHole1

Copy link
Copy Markdown
Member

What & why

The activation log's Reason column collapsed roughly ten distinct real-world triggers into four labels:

  • appActivated covered a real app switch, a launcher overlay (Spotlight/Raycast) taking focus, the overlay being dismissed, and the 1.5 s enhanced-mode URL poll re-check — indistinguishably.
  • lockEngaged covered the master toggle turning on, every config/rule edit, and the startup/restore apply.
  • The two most diagnostic facts — the source it switched away from, and the app or launcher behind the lock — were computed in the engine and then discarded.

So a user staring at the log saw a wall of "App activated" / "Lock engaged" with no way to tell what fired the switch, which app it was for, or why that source was chosen.

Changes

  • ActivationReason 4 → 9 cases: appActivated · launcherFocused · launcherDismissed · urlPolled · urlMatched · revertedSwitch · lockEngaged · configChanged · startupApplied. The reason is threaded from each engine entry point (handleLauncherChange splits on focus/dismiss, the URL poll loop emits urlPolled, apply(_:reason:) carries the caller's intent from AppState).
  • Four optional context fields on ActivationEvent / ActivationLogEntry: fromSourceName, triggeringBundleID + triggeringAppName, ruleSource, matchedHost. RuleResolver now reports which branch (app rule / global default / URL rule) produced the target.
  • Log UI: the source column renders X → Y, a new App column shows the triggering app/launcher, and a dimmed rule-source subtitle (plus the matched host for URL rows) sits under each reason.
  • i18n: 9 new catalog keys translated for all 8 non-English SupportedLanguage cases, matching existing terminology.

Notes

  • Migration-safe: the new SwiftData properties are all optional, so lightweight migration keeps a user's existing 24 h history; new enum raw values need no migration (reasonRaw/ruleSourceRaw are strings).
  • Semantics: the enabling force at apply() time is attributed to its own reason (startupApplied / lockEngaged / configChanged) with the URL provenance kept in ruleSource; urlMatched fires only when a post-lock URL change re-resolves the target. Covered by a dedicated test.

Testing

make test — 107 passing, 0 failures. New coverage across LockController, LockEngine, LogStore, RuleResolver, URLMatcher; LocalizationGuardTests green. An adversarial multi-agent review of the diff surfaced no correctness or i18n bugs.

The activation log's Reason column collapsed roughly ten distinct
real-world triggers into four labels: appActivated covered app
switches, launcher overlays, and URL poll re-checks alike, while
lockEngaged covered the master toggle, rule edits, and the startup
restore. The two most diagnostic facts — the source switched away
from, and the app or launcher behind the lock — were computed and
then discarded.

Expand ActivationReason to nine cases and carry four optional
context fields (from-source, triggering bundle/app name, rule
branch, matched URL host) from the engine into each event. The log
now renders "X -> Y", an App column, and a rule-source subtitle.
RuleResolver reports which branch produced the target; urlMatched
now fires only when a post-lock URL change re-resolves, while the
enabling force keeps its own reason and the URL provenance lives in
the rule branch.

The new SwiftData fields are optional so lightweight migration
preserves existing history; new enum raw values need no migration.
All nine new catalog keys are translated for every supported
language, and LockIMEKit coverage is extended for the new paths.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f5b7512-3465-44f9-bbfa-9e6adf20ab17

📥 Commits

Reviewing files that changed from the base of the PR and between 800d19b and 12e711f.

📒 Files selected for processing (2)
  • Sources/LockIMEKit/LockEngine/LockEngine.swift
  • Tests/LockIMEKitTests/LockEngineTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/LockIMEKit/LockEngine/LockEngine.swift

Summary by CodeRabbit

  • New Features
    • Activation log shows richer context: input-source transitions, triggering app display names, matched URL hosts, and rule-origin labels; reason column exposes more activation types (launcher focus/dismiss, URL poll/match, config/startup).
  • Internationalization
    • Added localized strings for launcher events, URL checks, settings changes, lock restoration, and rule labels in multiple languages.
  • Tests
    • Added tests validating activation reasons and persisted contextual log fields.

Walkthrough

This PR enriches the activation logging pipeline with a more granular taxonomy of activation reasons and contextual metadata tracking. New ActivationReason cases distinguish launcher focus/dismiss, URL polling vs rule matches, configuration changes, and startup enforcement. ActivationEvent now carries optional context (prior source name, triggering bundle ID, rule source attribution, matched URL host). A new RuleSource enum tags which precedence branch (app rule, global default, URL rule) produced a lock decision. The system propagates these details through LockController and LockEngine APIs, persists them via ActivationLogEntry, and displays richer context in the activation log UI table. AppState coordinates the new APIs, passing appropriate reasons for user actions and startup. Localization strings support the new UI labels.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AppState
  participant LockEngine
  participant RuleResolver
  participant LockController
  participant LogStore
  participant UI as ActivationLogPane

  User->>AppState: setMasterEnabled(true)
  AppState->>AppState: commit(reason: .lockEngaged)
  AppState->>LockEngine: apply(config, reason: .lockEngaged)
  LockEngine->>RuleResolver: resolve(...)
  RuleResolver-->>LockEngine: LockResolution.lock(source, .appRule)
  LockEngine->>LockController: setEnabled(true, reason: .lockEngaged)
  LockController->>LockController: force(fromSourceName, ruleSource, bundleID)
  LockController->>LockEngine: onActivation event
  LockEngine->>LogStore: record(ActivationEvent, triggeringAppName)
  LogStore->>LogStore: ActivationLogEntry persisted
  UI->>LogStore: fetch entries
  LogStore-->>UI: entries with context fields
  UI-->>User: display "source1 → source2" and rule source
Loading

Possibly related PRs

  • oomol-lab/LockIME#7: Adds shortcut handlers to AppState that invoke the same commit() and apply() paths now receiving activation reasons and context.
  • oomol-lab/LockIME#11: Modifies launcher-overlay focus handling in LockEngine to use FloatingAppMonitor and changes rule resolution context; overlaps with this PR's launcher focus/dismiss reason distinction.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required format with type 'feat' and scope 'log', and accurately describes the main changes: differentiating activation reasons and adding row context to the activation log.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem, specific changes made, and testing results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch improve-log

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/LockIMEKit/Logging/LogStore.swift (1)

41-47: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log the original error, not localizedDescription.

Both catch paths still stringify the Foundation-localized message, so diagnostics can flip to the system language and lose the original error payload. Log the error object (or stable domain/code fields) instead.

Suggested fix
-            Self.log.error("Failed to save activation log entry: \(error.localizedDescription, privacy: .public)")
+            Self.log.error("Failed to save activation log entry: \(String(describing: error), privacy: .public)")
...
-            Self.log.error("Failed to purge expired log entries: \(error.localizedDescription, privacy: .public)")
+            Self.log.error("Failed to purge expired log entries: \(String(describing: error), privacy: .public)")

As per coding guidelines, "Never display text localized by someone else's bundle. Map errors to semantic categories whose messages are catalog keys ... and log the original error instead. Avoid using Foundation/Sparkle error.localizedDescription which resolves against the system language."

Also applies to: 59-60

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LockIMEKit/Logging/LogStore.swift` around lines 41 - 47, In record(_
event: ActivationEvent, triggeringAppName: String? = nil) replace logging of
error.localizedDescription with logging the original Error object (or its stable
domain/code) so the full, non-localized payload is preserved; locate the catch
blocks in LogStore.save paths (including the second catch around lines 59–60)
and change Self.log.error("Failed to save activation log entry: ...") to include
the error itself (or error as NSError with domain/code) rather than
error.localizedDescription, keeping a clear descriptive message and the original
error payload.

Source: Coding guidelines

🧹 Nitpick comments (1)
Tests/LockIMEKitTests/LogStoreTests.swift (1)

20-45: ⚡ Quick win

Cover matchedHost in this round-trip test too.

This is the only new persisted field that still goes unexercised here, so a broken matchedHost mapping in ActivationLogEntry.init(_:) would slip through.

Suggested test tweak
         store.record(
             ActivationEvent(
                 timestamp: .now,
                 inputSource: "com.apple.keylayout.US",
                 inputSourceName: "U.S.",
                 reason: .revertedSwitch,
                 durationMs: 2.0,
                 fromSourceName: "Pinyin",
                 triggeringBundleID: "com.apple.Safari",
                 ruleSource: .appRule,
-                matchedHost: nil
+                matchedHost: "github.com"
             ),
             triggeringAppName: "Safari"
         )
@@
         `#expect`(row.triggeringAppName == "Safari")
         `#expect`(row.ruleSource == .appRule)
+        `#expect`(row.matchedHost == "github.com")
         `#expect`(row.reason == .revertedSwitch)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/LockIMEKitTests/LogStoreTests.swift` around lines 20 - 45, Update the
recordsContextFields test in LogStoreTests to exercise matchedHost: construct
the ActivationEvent with a non-nil matchedHost (e.g. "example.com") when calling
store.record and after fetching the ActivationLogEntry (rows.first) add an
assertion that row.matchedHost equals that value; this ensures
ActivationLogEntry.init(_:) correctly maps matchedHost along with
fromSourceName, triggeringBundleID, triggeringAppName, ruleSource, and reason.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/LockIMEKit/LockEngine/LockEngine.swift`:
- Around line 145-154: The switch branch that handles RuleResolver.resolve(...)
currently forces reason: ruleSource == .urlRule ? .urlMatched : reason; change
this so that when the incoming reason is an "apply-driven" reason (e.g.
.startupApplied, .lockEngaged, .configChanged passed by apply(reason:)), you
preserve that reason instead of overwriting it with .urlMatched; otherwise, if
the resolution truly originated from a URL and the incoming reason is not an
apply-driven type, set .urlMatched. Update the controller.setTarget(...) call in
the .lock case to conditionally choose .urlMatched only when ruleSource ==
.urlRule AND reason is not one of the apply-driven reasons, using
RuleResolver.resolve, ruleSource, urlMatch and the existing reason variable to
implement the check.
- Around line 93-96: The apply(_:reason:) flow can emit a forced switch because
reevaluate(reason:) runs while the controller is still in the old enabled state;
modify apply to check the incoming config.isEnabled and if it's false call
controller.setEnabled(false, reason: reason) before assigning self.config and
calling reevaluate(reason:) so the controller is disabled prior to reevaluation;
ensure you still call controller.setEnabled(config.isEnabled, reason: reason)
for the enabling path and keep reevaluate(reason:) after self.config assignment.

---

Outside diff comments:
In `@Sources/LockIMEKit/Logging/LogStore.swift`:
- Around line 41-47: In record(_ event: ActivationEvent, triggeringAppName:
String? = nil) replace logging of error.localizedDescription with logging the
original Error object (or its stable domain/code) so the full, non-localized
payload is preserved; locate the catch blocks in LogStore.save paths (including
the second catch around lines 59–60) and change Self.log.error("Failed to save
activation log entry: ...") to include the error itself (or error as NSError
with domain/code) rather than error.localizedDescription, keeping a clear
descriptive message and the original error payload.

---

Nitpick comments:
In `@Tests/LockIMEKitTests/LogStoreTests.swift`:
- Around line 20-45: Update the recordsContextFields test in LogStoreTests to
exercise matchedHost: construct the ActivationEvent with a non-nil matchedHost
(e.g. "example.com") when calling store.record and after fetching the
ActivationLogEntry (rows.first) add an assertion that row.matchedHost equals
that value; this ensures ActivationLogEntry.init(_:) correctly maps matchedHost
along with fromSourceName, triggeringBundleID, triggeringAppName, ruleSource,
and reason.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e6e4076a-7ac5-46f6-b3ec-5a3db544ee24

📥 Commits

Reviewing files that changed from the base of the PR and between 62cbdcf and 800d19b.

📒 Files selected for processing (15)
  • Sources/LockIME/AppState.swift
  • Sources/LockIME/Localizable.xcstrings
  • Sources/LockIME/UI/Settings/ActivationLogPane.swift
  • Sources/LockIMEKit/Enhanced/URLMatcher.swift
  • Sources/LockIMEKit/LockEngine/InputSource.swift
  • Sources/LockIMEKit/LockEngine/LockController.swift
  • Sources/LockIMEKit/LockEngine/LockEngine.swift
  • Sources/LockIMEKit/Logging/ActivationLogEntry.swift
  • Sources/LockIMEKit/Logging/LogStore.swift
  • Sources/LockIMEKit/Rules/RuleResolver.swift
  • Tests/LockIMEKitTests/LockControllerTests.swift
  • Tests/LockIMEKitTests/LockEngineTests.swift
  • Tests/LockIMEKitTests/LogStoreTests.swift
  • Tests/LockIMEKitTests/RuleResolverTests.swift
  • Tests/LockIMEKitTests/URLMatcherTests.swift

Comment thread Sources/LockIMEKit/LockEngine/LockEngine.swift Outdated
Comment thread Sources/LockIMEKit/LockEngine/LockEngine.swift
…ched

Address review on the activation-reason change. apply() now stops
enforcing before reevaluating a disabled config, so turning the lock
off never forces one last switch when the source has drifted off
target. And an apply-driven reason (lockEngaged / configChanged /
startupApplied) is preserved when its target resolves via a URL rule
on an already-enabled engine, instead of being overwritten with
urlMatched — the URL provenance is already carried by ruleSource.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@BlackHole1 BlackHole1 merged commit 68ecef3 into main Jun 12, 2026
3 checks passed
@BlackHole1 BlackHole1 deleted the improve-log branch June 12, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant