Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3b9699b
Add native ProductivityTracker iOS app with CI packaging.
cursoragent Aug 12, 2026
8157a77
Fix simulator selection on macOS awk.
cursoragent Aug 12, 2026
aaefa77
Fix Swift 6 App Intent static state and pin arm64 simulator builds.
cursoragent Aug 12, 2026
18021d3
Fix MainActor protocol conformance for timer and Live Activity.
cursoragent Aug 12, 2026
4b51167
Isolate AppRuntime from the widget and expand intent access.
cursoragent Aug 12, 2026
3ad9653
Fix notification authorization Sendable isolation.
cursoragent Aug 12, 2026
1dec76c
Avoid MainActor conflict with notification center delegate.
cursoragent Aug 13, 2026
30cdb23
Use targeted Swift 6 concurrency for Apple framework callbacks.
cursoragent Aug 13, 2026
e52b821
Avoid sending ActivityKit and notification objects across tasks.
cursoragent Aug 13, 2026
09887de
Set explicit product names for test bundles.
cursoragent Aug 13, 2026
1be3a16
Enable testability so unit tests can import the app module.
cursoragent Aug 13, 2026
b99421c
Import SwiftData in notification tests.
cursoragent Aug 13, 2026
e44cb10
Expose stable UI test identifiers for Space paging.
cursoragent Aug 13, 2026
c72ce3a
Disambiguate Space accessibility queries in UI tests.
cursoragent Aug 13, 2026
894190a
Use firstMatch for remaining space-name queries.
cursoragent Aug 13, 2026
42f5754
Force Work Space in screenshot mode and commit CI screenshots.
cursoragent Aug 13, 2026
9bafaa4
Correct timer semantics, Clock-like UI, and Live Activity.
cursoragent Aug 13, 2026
48ed48a
Remove TimerSnapshot from the Live Activity widget target.
cursoragent Aug 13, 2026
6a8b0c9
Fix Live Activity update concurrency isolation.
cursoragent Aug 13, 2026
598c930
Import ActivityKit as preconcurrency for Live Activity updates.
cursoragent Aug 13, 2026
232a17d
Mark TaskInterval test helper as throwing.
cursoragent Aug 13, 2026
9838240
Fix Live Activity task updates, default-task start, and UI queries.
cursoragent Aug 13, 2026
11f06ff
Put timer control identifiers on the Button views for XCTest.
cursoragent Aug 13, 2026
247ae67
Keep a single pair of timer controls outside the Space pager.
cursoragent Aug 13, 2026
8f6646f
Use plain SwiftUI Buttons for Lap, Reset, Start, and Stop.
cursoragent Aug 13, 2026
11b9a94
Overlay stopwatch controls on the canvas like Clock.
cursoragent Aug 13, 2026
1103836
Show stopwatch controls on every Space page for XCTest.
cursoragent Aug 13, 2026
cb5d027
Restore circle glassEffect so timer controls stay in the accessibilit…
cursoragent Aug 13, 2026
f632272
Show timer controls only on the visible Space page.
cursoragent Aug 13, 2026
bd460b7
Expose stopwatch accessibility only on the visible Space.
cursoragent Aug 13, 2026
5ca6c4b
Retrigger iOS CI now that the repository is public.
cursoragent Aug 13, 2026
835d803
Observe published timer snapshots in the stopwatch display.
cursoragent Aug 13, 2026
e88a7c0
Rename UITest label waiter to avoid XCTest overload.
cursoragent Aug 13, 2026
de25894
Arbitrate Lap tap versus long-press so chooser does not lap.
cursoragent Aug 13, 2026
ce9bbb1
Make Spaces, tasks, and Live Activity easier to use.
cursoragent Aug 14, 2026
6fae06c
Wait for pager and task-selection UI tests to settle.
cursoragent Aug 14, 2026
5a230d0
Polish Live Activity, paging, saves, and task taps.
cursoragent Aug 14, 2026
2473842
Select tasks on the first tap and complete on a second tap.
cursoragent Aug 14, 2026
9a18f69
Make Spaces paging snappy and keep Live Activity clocks stable.
cursoragent Aug 14, 2026
4f83370
Mark the compose-pull coordinator as unchecked Sendable for Swift 6.
cursoragent Aug 14, 2026
6b1d7df
Fix compose-pull math and restore native paging for UI tests.
cursoragent Aug 14, 2026
747bb9a
Add the unsigned device IPA zip for sideloading.
cursoragent Aug 14, 2026
b1f62f4
Make lock-screen stopwatch controls update ActivityKit first.
cursoragent Aug 16, 2026
63fab37
Fix Live Activity pause-time unit test optional comparison.
cursoragent Aug 16, 2026
cef57ec
Replace the sideload zip with the instant Live Activity build.
cursoragent Aug 16, 2026
885c4cd
Use an optimistic Live Activity Toggle for pause and resume.
cursoragent Aug 16, 2026
f8cd1c3
Replace the sideload zip with the optimistic Toggle Live Activity build.
cursoragent Aug 16, 2026
4ab4b68
Fix Live Activity resume after pause.
cursoragent Aug 16, 2026
3607d6e
Replace the sideload zip with the Live Activity resume fix.
cursoragent Aug 16, 2026
18dd6eb
Fix Live Activity remaining frozen after pause then play.
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .cursor/rules/productivity-tracker.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
description: ProductivityTracker is a native SwiftUI iOS timer app built from Windows via Cursor Cloud and GitHub Actions.
alwaysApply: true
---

# ProductivityTracker project rules

This repository is a native SwiftUI iOS app named ProductivityTracker.

- Primary authoring host is Windows.
- Cursor Cloud Agent runs on Linux.
- Never assume local Xcode, Simulator, or `xcodebuild` on the Linux agent.
- GitHub Actions `macos-26` is the authoritative build, test, screenshot, and unsigned IPA environment.
- After compilation-affecting changes, push and inspect GitHub Actions. Fix CI yourself.
- Keep XcodeGen `project.yml` as the project source of truth. Do not hand-edit `project.pbxproj` as the normal workflow. Pin XcodeGen via `.xcodegen-version`.
- Do not introduce React Native, Expo, Flutter, Capacitor, web-view shells, Firebase, CloudKit, backends, accounts, analytics, or paid services.
- Use public Apple APIs only. No private frameworks.
- Preserve native Apple visual language: system San Francisco, tabular stopwatch digits, iOS 26 Liquid Glass (`glassEffect` / `GlassEffectContainer`), semantic Start/Stop colors.
- The timer is the primary product. Do not turn the main screen into a dashboard.
- Only the upper stopwatch region handles horizontal Space paging. The lower task panel must stay stationary.
- Core app must remain compatible with the free personal-signing / Sideloadly installation path.
- No Apple credentials, signing certificates, provisioning profiles, or secrets in source or CI.
139 changes: 139 additions & 0 deletions .github/workflows/ios-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: iOS CI

on:
push:
branches: [main, master, cursor/**]
pull_request:
workflow_dispatch:

concurrency:
group: ios-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-test-package:
name: Generate, test, screenshot, package
runs-on: macos-26
timeout-minutes: 90
env:
SCREENSHOT_DIR: ${{ github.workspace }}/artifacts/screenshots
ARTIFACT_DIR: ${{ github.workspace }}/artifacts
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Print toolchain
run: |
echo "macOS: $(sw_vers -productVersion)"
echo "Xcode:"
xcodebuild -version
echo "Swift:"
swift --version
echo "Available Xcode apps:"
ls /Applications | grep -i xcode || true
echo "Simulator runtimes:"
xcrun simctl list runtimes || true

- name: Select Xcode 26
run: |
if [[ -d /Applications/Xcode_26.6.app ]]; then
sudo xcode-select -s /Applications/Xcode_26.6.app
elif [[ -d /Applications/Xcode.app ]]; then
sudo xcode-select -s /Applications/Xcode.app
fi
xcodebuild -version

- name: Install pinned XcodeGen
run: bash Scripts/ci/install-xcodegen.sh

- name: Generate Xcode project
run: |
xcodegen generate --spec project.yml
test -d ProductivityTracker.xcodeproj
xcodebuild -list -project ProductivityTracker.xcodeproj

- name: Select simulator
run: bash Scripts/ci/select-simulator.sh

- name: Build simulator
run: |
set -o pipefail
xcodebuild \
-project ProductivityTracker.xcodeproj \
-scheme ProductivityTracker \
-destination "platform=iOS Simulator,id=${SIM_UDID},arch=arm64" \
-configuration Debug \
ONLY_ACTIVE_ARCH=YES \
ENABLE_TESTABILITY=YES \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
build | xcbeautify --renderer github-actions

- name: Unit and UI tests
run: |
set -o pipefail
mkdir -p "$SCREENSHOT_DIR" "$ARTIFACT_DIR"
xcodebuild \
-project ProductivityTracker.xcodeproj \
-scheme ProductivityTracker \
-destination "platform=iOS Simulator,id=${SIM_UDID},arch=arm64" \
-configuration Debug \
-resultBundlePath "$ARTIFACT_DIR/TestResults.xcresult" \
ONLY_ACTIVE_ARCH=YES \
ENABLE_TESTABILITY=YES \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
test | xcbeautify --renderer github-actions

- name: Unsigned device build
run: |
set -o pipefail
mkdir -p "$ARTIFACT_DIR/device"
xcodebuild \
-project ProductivityTracker.xcodeproj \
-scheme ProductivityTracker \
-configuration Release \
-destination "generic/platform=iOS" \
-derivedDataPath "$ARTIFACT_DIR/DerivedData" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGN_IDENTITY="" \
AD_HOC_CODE_SIGNING_ALLOWED=YES \
build | xcbeautify --renderer github-actions
APP="$(find "$ARTIFACT_DIR/DerivedData" -path "*.app" -type d | grep "Release-iphoneos/ProductivityTracker.app$" | head -n 1)"
echo "APP=$APP" >> "$GITHUB_ENV"
test -n "$APP"
test -d "$APP"

- name: Package unsigned IPA
run: bash Scripts/ci/package-ipa.sh "$APP" "$ARTIFACT_DIR"

- name: Build metadata
run: |
{
echo "commit=$(git rev-parse HEAD)"
echo "ref=${GITHUB_REF}"
echo "xcode=$(xcodebuild -version | tr '\n' ' ')"
echo "macos=$(sw_vers -productVersion)"
echo "simulator_name=${SIM_NAME}"
echo "simulator_os=${SIM_OS}"
echo "simulator_udid=${SIM_UDID}"
echo "ipa=conduit-unsigned.ipa"
echo "sha256=$(cut -d ' ' -f1 "$ARTIFACT_DIR/SHA256SUMS.txt")"
echo "UNSIGNED=must be signed with your own Apple Account before installation"
} | tee "$ARTIFACT_DIR/build-metadata.txt"

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: conduit-iOS-device-unsigned
if-no-files-found: error
retention-days: 30
path: |
artifacts/conduit-unsigned.ipa
artifacts/SHA256SUMS.txt
artifacts/build-metadata.txt
artifacts/screenshots
artifacts/TestResults.xcresult
48 changes: 48 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Xcode
DerivedData/
build/
*.xcuserstate
*.xccheckout
*.xcscmblueprint
xcuserdata/
*.moved-aside
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
*.xcresult

# Generated project (CI regenerates from project.yml)
*.xcodeproj/
*.xcworkspace/
!project.yml

# Swift PM
.swiftpm/
.build/
Package.resolved

# macOS
.DS_Store
*.swp
*~

# Secrets / signing
*.p12
*.cer
*.certSigningRequest
*.mobileprovision
*.provisionprofile
AuthKey_*.p8
*.keychain
*.keychain-db

# Python
__pycache__/
*.pyc

# Local helper output
artifacts/
dist/
.idea/
.vscode/
1 change: 1 addition & 0 deletions .xcodegen-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.46.0
97 changes: 97 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Architecture

## Timer state model

Each Space owns a `TimerEngine` / `TimerSnapshot`. At most one Space is `running`.

User-visible phases match Apple Clock:

- `idle` — `00:00.00`; Lap disabled; Start
- `running` — advancing; Lap; Stop
- `stopped` — frozen and resumable; Reset; Start

`Stop` freezes elapsed, closes the open task interval, and leaves `Session.endedAt` nil. `Start` from stopped resumes the same session. `Reset` archives one historical session (`endedAt` set), dismisses the Live Activity immediately, and returns the display to idle.

Persisted `"paused"` maps to `stopped`.

## Timestamp calculation

`TimerSnapshot` stores `accumulatedBeforeCurrentRun` plus `startedAt` while running.

```
idle: 0
stopped: accumulatedBeforeCurrentRun
running: accumulated + (now - startedAt)
```

Truth never depends on frame count.

## Persistence

SwiftData models:

- `Space` — name, accent, order, reminder seconds (`0` = Off), Focus keyword, default task, icon (SF Symbol, emoji, or monogram)
- `TaskItem` — name, order, enabled, completed
- `Session` — space, start, optional `endedAt` (nil = current/resumable), phase, accumulated, active task
- `TaskInterval` — session, task, start, optional end

Open sessions restore per Space. If more than one was stored as running, extras are frozen.

## Task intervals

Start opens an interval for the selected / default / first enabled task. Direct task tap while running closes the previous interval and opens the new one at the same timestamp. Same-task tap is a no-op. Lap advances to the next enabled task. Deleting the active task moves to the next enabled task or continues with no task.

Task-row totals are the current stopwatch session only. They clear on Reset. History keeps archived totals.

## Task list interaction

Each Space page includes its task list on the same full-page canvas (not a separate lower card region).

- Single tap selects the task for timing while running.
- Double-tap toggles complete / incomplete.
- Context menu: Complete / Mark Incomplete, Rename, Enable / Disable, Delete.
- Inline **Add a task** field at the bottom of the list.

## Space paging

A page-style `TabView` fills the screen. Each page is one Space: header (icon + name), stopwatch, Lap/Start/Stop/Reset controls, page dots, and tasks together. Swiping a Space does not move elapsed time onto the newly visible Space. Starting Space B while A is running freezes A, then starts or resumes B.

A trailing **New Space** page (Apple Home Screen–style extra page) lets you compose a Space before it exists: name, initial tasks, accent color, and icon (SF Symbols, emoji, or monogram from the name). Creating it selects that Space and returns to its page.

Space tint is a top-to-bottom color wash on black, not giant tinted glass cards.

## Live Activity

`SessionActivityAttributes.ContentState` carries space name, current task, running flag, elapsed, tint, and workspace icon.

Lock Screen layout:

- Large space-colored **space name**
- Current task subtitle
- Elapsed via `Text(timerInterval:countsDown:false, showsHours:true)` while running. Stopped uses the same formatter with `pauseTime`. Resume **replaces** that view (new `displayStart` / `elapsedClockID`) instead of clearing `pauseTime` on the paused instance — the system timer does not unfreeze otherwise.
- Explicit **Stop** (`StopFromLiveActivityIntent`) or **Start** (`ResumeFromLiveActivityIntent`) plus **Reset** (`xmark`). Not a `Toggle` / `SetValueIntent` (that control can pause twice and never resume).
- Large workspace icon on the trailing edge
- Background tint from the Space color (`activityBackgroundTint`)

Default activity taps do not mutate the timer (`LiveActivityPresentation.backgroundMutatesTimer == false`). Stop updates the activity as frozen. Reset dismisses with `.immediate`.

## App Intents

Intents call `AppRuntime.shared.sessionController`. Pause intents freeze like Stop. Resume intents call Start. Focus Filter selects a Space. No backend.

## Notifications

`Distraction Started` schedules a local reminder only if a timer is running and the Space reminder is not Off. Copy is `Work · 42:18` / `Timer is still running.` Actions: Continue, Stop.

## Test architecture

- Engine and formatter unit tests
- Stopwatch semantics (stop / resume / reset)
- Task interval and editing tests
- Per-Space ownership tests
- Live Activity state tests
- UI tests: full-page paging isolation, labels, long-press Lap, compose page, screenshots, Live Activity preview canvas

## CI architecture

Linux agent never runs Xcode. `.github/workflows/ios-ci.yml` on `macos-26` generates the Xcode project, tests, packages `conduit-unsigned.ipa` (`Payload/ProductivityTracker.app` inside), and uploads `conduit-iOS-device-unsigned`.
84 changes: 84 additions & 0 deletions Design/NATIVE_IOS_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Native iOS audit

Research for the ProductivityTracker corrective pass. No third-party source was copied or vendored.

## Repositories inspected

All four requested apps are currently maintained (GitHub API, 13 Aug 2026):

| Project | Evidence | Inspected for |
| --- | --- | --- |
| [Ranchero-Software/NetNewsWire](https://github.com/Ranchero-Software/NetNewsWire) | Last commit 12 Aug 2026 (`Update appcasts.`) | Feed list editing, grouped settings, absence of decorative cards |
| [mastodon/mastodon-ios](https://github.com/mastodon/mastodon-ios) | Last commit 10 Jul 2026 | UIKit table editing, swipe actions, system navigation |
| [Dimillian/IceCubesApp](https://github.com/Dimillian/IceCubesApp) | Last commit 9 Jun 2026; App Store client | SwiftUI lists, context menus, native sheets |
| [jellyfin/Swiftfin](https://github.com/jellyfin/Swiftfin) | Last commit 12 Aug 2026 (Weblate) | Settings forms, playback chrome vs content |

None were stale enough to replace.

Useful files (read on GitHub, not imported): NetNewsWire iOS Settings storyboards / inspector lists; IceCubes `App/Main/Settings`; Swiftfin Settings views. Pattern: **plain grouped lists, EditMode, swipe-to-delete, context menus**. No giant tinted content cards.

## Apple documentation consulted

- Human Interface Guidelines: layout, lists, buttons, haptics, motion
- SwiftUI: `List` + `EditMode`, context menus, `TimelineView(.animation)`
- WWDC25 “Build a UIKit app with the new design”: Liquid Glass is a **navigation/control layer**, not content wallpaper
- ActivityKit / Live Activities: timer-first lock screen presentation; `Text(timerInterval:countsDown: false)` for count-up; `ActivityUIDismissalPolicy.immediate` on end
- App Intents: explicit buttons only; default Live Activity tap opens the app
- Public Clock stopwatch behavior: Idle Lap+Start, Running Lap+Stop, Stopped Reset+Start; Stop freezes; Start resumes; Reset clears

## Principles adopted

1. Content sits on system black. Glass is not a background.
2. Stopwatch controls are independent circular buttons, not a joined glass capsule.
3. Lists use native `List` / row separators, EditMode, swipe, context menu.
4. Live Activity: elapsed time first; one explicit control; no `xmark` for Stop; no pause-on-surface-tap.
5. Motion: native paging physics; no extra springs on TabView; Reduce Motion respected.
6. Copy is functional (`Start`, `Stop`, `Reset`, `Timer is still running.`).

## Patterns explicitly rejected

- Tinted Liquid Glass on half-screen squarcles (`GlassSurface` on timer + task panel)
- `GlassEffectContainer` wrapping Lap/Start so they visually merge
- Stop = finalize/archive (previous model)
- Start after Stop = reset then new session
- Pause icon + X as primary Live Activity chrome
- Binding Pause to the whole Live Activity
- Tutorial empty-state paragraphs
- 0.07s periodic TimelineView (visible stepping)
- One global timer snapshot relabeled by swipe

## How this changed ProductivityTracker

- Per-Space `TimerEngine` ownership; swipe never relabels a running session.
- Stop freezes; Start resumes; Reset archives and clears.
- Main screen is a black Clock-like canvas. `GlassSurface` is unused on timer and task regions.
- Space accent is a 7pt indicator and page dots, not a fill.
- `TimelineView(.animation)` drives only stopwatch digits (and the live active-task time), from timestamps.
- Native Form/List editing for Spaces and tasks, including rename and reorder.
- Live Activity redesigned: timer first, one control, space name in ContentState, no pause-on-tap, immediate dismiss on Reset.

## Follow-up — Conduit branding and UX (Aug 2026)

Prior audit conclusions stand (native lists, per-Space engines, Stop/Start/Reset semantics). Subsequent work rebranded the product to **Conduit** on the home screen while keeping bundle ID `com.arahe.ProductivityTracker` and Xcode target names.

**Visual**

- App icon: liquid-glass amber **C** / conduit mark (replaces egg-timer dial).
- Main canvas: Space tint as a subtle top color wash on black, not half-screen tinted glass cards.

**Navigation**

- Full-page Space paging: timer, controls, and tasks share one page; swipe moves the whole Space.
- Trailing compose page (Home Screen–style “+” page): name, tasks, color, icon picker (SF Symbols, emoji, monogram).

**Tasks**

- Double-tap and context menu complete / uncomplete tasks.
- Inline add-task field on each Space page.

**Live Activity**

- Large space-colored name, current task, `Text(timerInterval:showsHours:true)` while running and with `pauseTime` when stopped (no ms). Resume uses a new clock identity, not an unpaused `pauseTime`.
- Stop / Start plus Reset controls; large workspace icon; background tint from Space color.
- Stop still freezes; Start resumes; Reset archives and dismisses the Live Activity immediately.

Loading
Loading