Plan tab
PlanTabViewtakes the first slot in the tab bar. ATodayHeroCardshows today’s planned item up top — a strength session, a running workout, or a rest day — with the appropriate primary action (Start, Send to watch, or nothing). Below it,WeekStripViewlays out the six other days of the week as a horizontal strip; tapping a day scrolls the rest of the screen to that day’s detail.- Library, running and strength side by side.
WorkoutLibraryViewand the running plan templates share a single picker surface, with discipline chips (Running coral, Strength / Bodyweight neutral) so the two disciplines never look pasted together. PlanItemLinker. FinishedHKWorkouts — including the ones recorded on a Garmin watch and synced via Apple Health — back-link to the closest uncompletedPlanItemwhen the match score on duration / distance / time-of-day is ≥ 70 %. The Today card flips to “completed” without the user filing anything by hand.
A plan is the memory the rest of the app reads from. Without one, every session was a one-off — useful as a log, useless as a build. The Plan tab puts a six-day horizon in front of the user every time they open the app.
AI coach loop
CoachExporter. Builds a prompt containing the user’s goal, the current plan state (active plan, recent sessions, pain trend if rehab is on), and the workout schema, in a single pasteable block.- Chatbot picker sheet. Three buttons — Claude, ChatGPT, Gemini — each backed by a universal link that iOS routes into the chatbot’s iOS app if installed, falling back to the web. Tapping a button copies the prompt to the clipboard and opens the chat; the user pastes, sends, and the chatbot interviews them before emitting JSON.
PlanImporter. The bot saves the plan JSON toiCloud Drive → Aski → plans.NSMetadataQuerywatches the folder and pulls the new plan in without an explicit sync trigger — the same mechanism that has been watchingworkouts/since Foundation.
Track, don’t coach still holds. The coach lives in your chatbot, where it belongs; Aski stores the result and runs the sessions. The app is the schedule and the receipt. Nothing on Aski’s side ever calls a chatbot API — the handoff is a clipboard and a universal link, end of integration.
Bundled starter plans
- Lifting. Four 12-week starters drawn from the r/Fitness wiki:
Basic Beginner Routine, GZCLP, 5/3/1 for Beginners, and the
Bodyweight Fitness Recommended Routine. Each plan ships with its
attribution metadata intact (
coachAttribution), so the source is visible in the plan detail view. - Running. The existing Pfitzinger and Hal Higdon marathon plans stay where they were, now alongside the lifting set in the same starter picker.
- One-screen onboarding.
OnboardingFirstPlanViewcollapses the three-stage flow into a single screen with a coral Write one with my AI coach CTA and a neutral Use a bundled plan picker. Everything else (Strava, notifications, Watch setup) defers to Profile until the user actually needs it — invariant #5 intact.
.fit export to FIT-capable watches
FitWorkoutEncoder(~500 lines, no deps). Encodes a runningPlanItemto binary FIT with the message types Garmin’s workout importer expects:file_id,workout, and N ×workout_step, withworkout_step.duration_typecovering distance, time, lap button, and repeat-from for nested repeat blocks. HR-zone and pace-zone targets pass through. CRC-16 over the full file.FitWorkoutFileWriterwrites the byte buffer to a temporary.fitfile with a slug-derived filename.- Send to watch. A
SendToWatchSheetis reachable fromRunningWorkoutDetailViewand the Plan-tab Today hero. It explains the two-step Garmin Connect mobile import flow and vends the.fitthrough the iOS share sheet —UIActivityViewController, the standard target picker. Aski has no awareness of where the file lands after that: Garmin Connect, AirDrop to a Wahoo, email to a COROS user, all of those work.
One-way, off by default, and explicitly not the same thing as a
Garmin Connect API integration. The Garmin Developer Program
application paperwork is filed (docs/garmin-application.md); until
that approval lands, a .fit through the share sheet is the
deterministic, user-controlled fallback. Real-device validation
across watches is ongoing — silent encoding mismatches are exactly
the kind of thing that can survive a unit test, so this surface gets
a “Phase-2 in progress” tag in the privacy policy and the support
page.
IA reshape
- Settings → Profile.
ProfileTabViewreplacesSettingsView. Six sections in fixed order: Plans (active card + Past plans), Coach loop (iCloud sync state, Open plans folder in Files shortcut), Connections (Strava live, Garmin Phase-2 stub, Apple Health), Training (celebrate PRs, 24h check, keep-screen-awake, mirror form videos to Photos), Watch (rest haptic, tempo haptic, also-tick-on-phone, Watch link log), About (language, how-it-works, version). The fullDiagnosticsViewis hidden behind 5 taps on the version row. - History → Report.
ReportTabViewreplaces the History tab. APlanProgressCardsits on top — completed days, upcoming days, pain trend if rehab is on — with a Send to coach action that drops a session summary into the sameCoachExporterflow. PainReactivityCard. ConsolidatesPainGateSheetandMorningCheckViewinto a single component that’s surfaced inline on the session summary and from the 24h-check notification. Same triggers, fewer code paths.- No new tab bar visibility. The runner still hides the tab bar
during a session, owns its
HKWorkoutSession, and never lets the watch end the workout — invariants #3 and #6 intact.
Strings & under-the-hood
- Phase-1 string extraction sweep into
Localizable.xcstrings—tab.plan,tab.report,tab.profile,onboarding.aiCoach,onboarding.starterPicker.*,onboarding.handoff.*,profile.section.*, plus the FIT export and chatbot-handoff copy. library.titlekey collision fix betweenWorkoutLibraryViewandLibraryListView— renamed tolibrary.freeWorkouts.titlewith EN + NL translations.- Garmin Developer Program application pack:
docs/garmin-application.md. The privacy policy already lists Garmin Connect as Phase 2 (Activity API for runs in, Training API for runs out, tokens in Keychain, revokable). Nothing in Connections → Garmin is wired up yet — the row is a stub until approval lands.
Tests
FitWorkoutEncoderTestscovers the message-encoding round-trip, CRC-16, repeat-block nesting, and HR / pace target field widths (357 lines).PlanImporterTestscovers the iCloud → SwiftData hand-off, including theplans/folder watch and the conflict path when a plan with the sameidalready exists.OnboardingFirstPlanViewtests cover the chatbot-picker copy-then-open flow and the bundled-plan attribution propagation.