Architecture¶
GrotTrack is a single Xcode project (GrotTrack.xcodeproj, generated from project.yml by XcodeGen) producing two build targets, plus a separate TypeScript Chrome extension project. This page describes the real component boundaries in the source. The repository also carries arch.txt, a longer-form architecture document maintained by hand; where the two disagree, this page and the source under GrotTrack/ win — arch.txt is a design record, not always current with every refactor (for example it lists two "section 7" headings, MULTITASKING DETECTION and CHROME EXTENSION ARCHITECTURE, which is a copy-paste numbering slip rather than a structural statement).
Two Xcode targets, one project¶
project.yml defines:
GrotTrack— the menu bar application. Typeapplication,SWIFT_STRICT_CONCURRENCY: complete, bundle IDcom.grottrack.app, deployment target macOS 26.0. It depends on thelibwebpandSparkleSwift packages and embedsGrotTrackNativeHostas a copied executable inside its own bundle (Contents/MacOS/).GrotTrackNativeHost— atooltarget, bundle IDcom.grottrack.nativehost. Its sources areGrotTrackNativeHost/main.swiftplus two files shared directly from the app target:GrotTrack/Services/NativeMessageHost.swiftandGrotTrack/Utilities/SharedConstants.swift. There is no shared framework target — the host is compiled as its own binary with those two files included by path.GrotTrackTests— abundle.unit-testtarget that runs against theGrotTrackapp as its test host.
Regenerate the Xcode project with xcodegen generate after any change to project.yml or after adding/removing source files — see Building.
MVVM with @Observable¶
The app follows MVVM: SwiftUI Views/ read from @Observable view models (ViewModels/AppState.swift, TimelineViewModel.swift, TrendReportViewModel.swift), which in turn read from the service layer and SwiftData. GrotTrackApp.swift defines AppCoordinator, an @Observable @MainActor class that is the single composition root: it constructs ActivityTracker, ScreenshotManager, BrowserTabService, IdleDetector and TimeBlockAggregator, and wires the SwiftData ModelContext into each service once the ModelContainer is ready in the app's .task modifier.
The project builds with Swift 6 strict concurrency (SWIFT_STRICT_CONCURRENCY = complete). AppState and AppCoordinator are @MainActor; service classes that cross isolation boundaries are Sendable.
Data flow¶
ActivityTrackerpolls the frontmost application every 3 seconds viaAXUIElementand listens toNSWorkspaceactivation notifications. It reads app name, bundle ID and window title, and — if the Chrome extension is connected — the active tab's title and URL fromBrowserTabService. Each poll writes anActivityEventto SwiftData.ScreenshotManagercaptures the screen every 30 seconds (configurable 15–120s) via ScreenCaptureKit, scales to a 1280px long edge, encodes as WebP at 80% quality, generates a thumbnail, and writes both to disk plus aScreenshotmetadata record.MultitaskingDetectormaintains a rolling 5-minute window of app switches and combines it withCGWindowListCopyWindowInfovisible-window counts to produce a 0.0–1.0 focus score, stored on bothActivityEventandTimeBlock.TimeBlockAggregator.aggregateHour()runs at the top of each hour and groups the hour'sActivityEventrecords into aTimeBlockwith a dominant app, dominant title and multitasking score. The live timeline view readsActivityEventrecords directly (notTimeBlocks) and groups them in memory for display;TimeBlocks exist forReportGenerator's trend reports.ReportGeneratoraggregatesTimeBlocks intoWeeklyReport/MonthlyReportSwiftData records with app allocations, daily focus scores and daily app-hours, each stored as a JSON string field rather than a nested@Model.
Screenshot enrichment pipeline¶
After a screenshot is saved, ScreenshotEnrichmentService runs asynchronously:
- OCR via Vision's
VNRecognizeTextRequestproduces a full transcript. EntityExtractorparses that transcript for structured entities — URLs, file paths, git branches, ticket references and similar patterns.- A
ScreenshotEnrichmentrecord is persisted with the OCR text, a shorttopLinessummary, the extracted entities as JSON, and astatus(pending/running/completed/failed).
Independently, SessionDetector groups consecutive ActivityEvents into ActivitySession records by app-switch and idle-gap boundaries. When Apple Intelligence is available (macOS 26+, via the FoundationModels framework), SessionClassifier labels each session with a short human-readable task name (for example "Code Review") and a confidence score, entirely on-device. Both enrichment paths degrade gracefully — OCR and entity extraction only need Screen Recording permission; classification is skipped when Apple Intelligence is unavailable, and the UI displays whatever enrichment exists.
Browser tab tracking¶
This is the one path that leaves the app process. See Native Messaging for the full wire protocol; in outline:
- The Chrome extension's background service worker watches
chrome.tabsevents and pushes the active tab's title, URL, tab ID, window ID and timestamp toGrotTrackNativeHostover Chrome's native messaging stdio protocol, debounced 300ms. GrotTrackNativeHost(running as a Chrome-launched subprocess) decodes the message and broadcasts it locally viaNSDistributedNotificationCenter— a same-machine, same-user IPC mechanism, not a network call.BrowserTabService, running insideGrotTrack.app, observes that notification and caches the latest tab. It considers the data stale after 10 seconds with no update.ActivityTrackerreadsBrowserTabService.activeTabTitle/.activeTabURLon its normal 3s poll cycle rather than being pushed to directly.
This design was chosen specifically to avoid needing the macOS Automation (Apple Events) entitlement that a JXA-based tab reader would require.
Storage¶
All data lives under ~/Library/Application Support/GrotTrack/:
GrotTrack.store — SwiftData database (SQLite-backed)
Screenshots/YYYY-MM-DD/ — full-resolution WebP screenshots
Thumbnails/YYYY-MM-DD/ — WebP thumbnails
Exports/ — exported reports and LLM evidence bundles
Four SwiftData @Model types are registered in GrotTrackApp.init(): ActivityEvent, Screenshot, TimeBlock, DailyReport. ScreenshotEnrichment and ActivitySession are additional @Model types used by the enrichment pipeline; WeeklyReport and MonthlyReport back the trend-report views. AppAllocation and the other report sub-structures are plain Codable structs serialized to JSON inside their owning model, not separate @Model types.
Retention is configurable: full screenshots default to 7 days, thumbnails to 30 days. Cleanup runs automatically at launch and can also be triggered manually from Settings.
There is no App Sandbox (GrotTrack.entitlements sets com.apple.security.app-sandbox to false) and no external network calls from the app itself — see Privacy for the full data-handling statement.
App visibility and lifecycle¶
Info.plist sets LSUIElement to true, so GrotTrack has no Dock icon and lives entirely in the menu bar (MenuBarExtra). NSSupportsAutomaticTermination and NSSupportsSuddenTermination are both false so the system does not silently terminate the app while it is meant to be tracking in the background. Auto-update is handled by the Sparkle framework; see Releasing.