Skip to content

feat(#646): label set types in workout UI#647

Merged
9thLevelSoftware merged 1 commit into
mainfrom
feat/issue-646-set-type-labels
Jul 10, 2026
Merged

feat(#646): label set types in workout UI#647
9thLevelSoftware merged 1 commit into
mainfrom
feat/issue-646-set-type-labels

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

What

Labels each set in the workout UI as Calibration Reps (first 3 ROM/calibration reps), Warm-up Set N (variable warm-up phase), or Working Set (routine default). Visible on the active workout HUD below the existing "Set X / Y" text and inline in the Set Ready header. Just Lift is unchanged.

Why

Issue #646 reporter frequently forgets they have Warm-up Sets enabled for some exercises because the UI doesn't currently distinguish the firmware 3-rep ROM buffer (labeled "REP" / "WARMUP" in the rep counter) from variable warm-up sets and from working sets.

Scope (bounded)

  • 1 pure-Kotlin helper setTypeLabel(repCount, currentWarmupSetIndex, showCalibrationLabel) returning sealed Calibration | Warmup(n) | Working — no Compose runtime, no string resources, trivially testable.
  • 4-line label render in ExecutionPage's existing !isJustLift && exerciseName != null block (reuses existing weight, color, and spacing).
  • Inline label in SetReadyScreen header next to the existing set picker (reuses existing mode picker typography).
  • 6 locale string resources (English-default fallback acceptable per project pattern).
  • 1 new param currentWarmupSetIndex: Int = -1 plumbed WorkoutTabWorkoutHudExecutionPage. SetReadyScreen collects viewModel.currentWarmupSetIndex directly.
  • 4 pure-Kotlin helper unit tests.
  • New showCalibrationLabel = false flag at the SetReady call site (calibration reps run during the active set, not at SetReady).

Not in scope (unchanged)

  • No BLE / protocol / packet change
  • No DB migration, no schema change
  • No SetSummaryCard Echo phase breakdown change
  • No rename of the existing "REP" / "WARMUP" rep-counter labels
  • No new design tokens (color, typography, shape)
  • No telemetry / analytics hooks
  • ActiveSessionEngine, RoutineFlowManager, WorkoutCoordinator untouched

Compatibility

  • Additive i18n keys only — no existing strings change.
  • No data shape change — the label is purely derived from existing state.
  • Old routines, old logs, all render correctly without migration.

Risks

  • Drift between label and engine behavior — mitigated by wiring the helper off the same currentWarmupSetIndex and repCount fields the engine already uses for runtime behavior.
  • Locale gap — non-English locales get English fallback; future i18n PRs can localize per project pattern.
  • Calibration label false-positive at SetReady — fixed by the showCalibrationLabel = false flag at that call site.
  • Future firmware buffer change — the literal 3 in the helper is tied to Constants.DEFAULT_WARMUP_REPS; ponytail: comment in code documents the coupling.

Tests

  • New SetTypeLabelTest.kt — 4 pure-Kotlin helper tests, all green.
  • Existing :shared:testAndroidHostTest suite (2431 tests) — 0 failures.
  • Manual smoke on AVD required: see Visual Evidence section.

Visual Evidence

To be added as a PR comment once emulator captures are complete (final-visuals/android/*.png).

References

  • Architecture: phoenix-enhancements/issue-646-1525152789725188258/architecture.md
  • Integration Model: phoenix-enhancements/issue-646-1525152789725188258/integration-model.md
  • Implementation Spec: phoenix-enhancements/issue-646-1525152789725188258/implementation-spec.md
  • Signoff (approved 2026-07-10 by 9thLevelSoftware): phoenix-enhancements/issue-646-1525152789725188258/signoff.md
  • Prototype: https://gist.github.com/9thLevelSoftware/5bf123a90c87aef44337aaad58dfc6a7

Fixes #646

- Pure-Kotlin setTypeLabel() helper returns sealed Calibration | Warmup(n) | Working
  off existing repCount.warmupReps/isWarmupComplete + coordinator.currentWarmupSetIndex
- WorkoutHud ExecutionPage renders label inline under the existing
  'Set X / Y' text; rides the existing !isJustLift guard
- SetReadyScreen header renders warmup/working label inline with
  the existing mode picker; calibration label suppressed
- 6 locale strings.xml: set_type_calibration | set_type_warmup | set_type_working
- WorkoutTab forwards currentWarmupSetIndex from state through to WorkoutHud
- 4 pure-Kotlin helper tests; existing 2431-test suite still green

Fixes #646

// ponytail: 3 mirrors Constants.DEFAULT_WARMUP_REPS; revisit if firmware
// calibration buffer size ever changes.
private const val CALIBRATION_BUFFER_REPS = 3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔥 The Roast: You wrote CALIBRATION_BUFFER_REPS = 3 and then helpfully left a ponytail: comment telling future-you it mirrors Constants.DEFAULT_WARMUP_REPS. That's a Post-it note on a landmine. The engine uses Constants.DEFAULT_WARMUP_REPS in ~10 call sites; the day someone bumps that constant for a firmware change, this helper silently disagrees with the engine and the UI cheerfully labels live ROM reps as "Working Set."

🩹 The Fix: Delete CALIBRATION_BUFFER_REPS and import com.devil.phoenixproject.util.Constants, then reference Constants.DEFAULT_WARMUP_REPS directly in the when clause. One source of truth, one landmine defused. Add a regression test that exercises a non-3 buffer size if you want belt-and-suspenders.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

val setTypeText = when (setType) {
is SetTypeLabel.Warmup -> stringResource(Res.string.set_type_warmup, setType.setNumber)
SetTypeLabel.Working -> stringResource(Res.string.set_type_working)
SetTypeLabel.Calibration -> "" // unreachable; showCalibrationLabel=false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔥 The Roast: Calibration -> "" annotated // unreachable, wrapped in if (setTypeText.isNotEmpty()). The compiler can prove the branch is dead; the comment admits it; the runtime guard defends against it. Three layers of "I know this is awkward" stacked on a helper that knows too much.

🩹 The Fix: Split the helper. Keep setTypeLabel(repCount, currentWarmupSetIndex) returning Calibration | Warmup(n) | Working for the active set (ExecutionPage). Add a sibling readySetTypeLabel(repCount, currentWarmupSetIndex) returning just Warmup(n) | Working for SetReady. The unreachable branch and the isNotEmpty() guard both vanish; the call sites read more honestly.

📏 Severity: suggestion


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 1
💡 suggestion 1
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
shared/.../util/SetTypeLabel.kt 26 Hardcoded CALIBRATION_BUFFER_REPS = 3 duplicates Constants.DEFAULT_WARMUP_REPS — a Post-it note on a landmine.
shared/.../screen/SetReadyScreen.kt 452 Calibration -> "" with // unreachable plus isNotEmpty() guard — three layers of "I know this is awkward."

Correctness / Safety Findings

  • important: SetTypeLabel.kt:26CALIBRATION_BUFFER_REPS = 3 duplicates Constants.DEFAULT_WARMUP_REPS (util/Constants.kt:35). The engine reads the constant in 10+ call sites; the helper is the only one that hardcodes the literal. When the firmware buffer size changes (the engine already preserves the 3-rep assumption with a comment at ActiveSessionEngine.kt:2620), this helper will silently mislabel calibration reps. Fix: delete the local constant, import Constants, reference Constants.DEFAULT_WARMUP_REPS directly.

  • minor: SetReadyScreen.kt:452-454Calibration -> "" branch (annotated // unreachable) wrapped in if (setTypeText.isNotEmpty()) is dead code defended at runtime. Fix: split into two helpers (one for active set returning the sealed type, one for SetReady returning only Warmup | Working), or have the helper return String? when calibration is suppressed.

Ponytail Review

  • shared/.../util/SetTypeLabel.kt:26: reuse — CALIBRATION_BUFFER_REPS = 3 duplicates Constants.DEFAULT_WARMUP_REPS. Replace with Constants.DEFAULT_WARMUP_REPS and delete the local const + the ponytail: apology comment.
  • shared/.../screen/SetReadyScreen.kt:452: shrink — Calibration -> "" branch and isNotEmpty() guard exist only because one helper serves two call sites with different needs. Replace with a readySetTypeLabel(...) sibling that returns just Warmup | Working.
  • shared/.../screen/SetTypeLabelTest.kt: test-shrink — only Warmup(setNumber = 1) is covered; add one test for setNumber = 2 so the +1 arithmetic isn't load-bearing without coverage. (Optional.)

Ponytail net: -8 lines.

Suggested Minimal Patch

  1. SetTypeLabel.kt: drop CALIBRATION_BUFFER_REPS, import com.devil.phoenixproject.util.Constants, replace with Constants.DEFAULT_WARMUP_REPS in the when. Delete the now-stale ponytail: comment.
  2. SetReadyScreen.kt: introduce a second helper readySetTypeLabel(repCount, currentWarmupSetIndex): Warmup | Working in util/SetTypeLabel.kt; switch the SetReady call site to it; delete the Calibration -> "" branch and the isNotEmpty() guard.

That's two files, one behavior-preserving change, one drift fix. No new dependencies, no tests removed.


🏆 Best part: The pure-Kotlin + sealed-type helper. Trivially testable, no Compose runtime, no string-resource coupling at the decision site. That's the right instinct.

💀 Worst part: Hardcoding a value the rest of the engine reads from a shared constant, then writing a ponytail: comment to apologize to your future self instead of just using the constant.

📊 Overall: A tight, well-scoped PR that earns its keep on the helper design — but trips on the one constant it shouldn't have defined. Fix the 3, fold the dead branch, ship it.

Final Merge Guidance

Can merge after the warning finding (CALIBRATION_BUFFER_REPS duplication) is fixed. The suggestion-level finding is optional cleanup that can land in a follow-up.

Fix these issues in Kilo Cloud

Files Reviewed (11 files)
  • shared/.../composeResources/values/strings.xml — 3 new keys, additive only.
  • shared/.../composeResources/values-{de,es,fr,it,nl}/strings.xml — locale fallbacks (English-default per project pattern).
  • shared/.../presentation/screen/SetReadyScreen.kt — see line 452 finding.
  • shared/.../presentation/screen/WorkoutHud.kt — clean parameter plumbing, conditional Text only.
  • shared/.../presentation/screen/WorkoutTab.ktcurrentWarmupSetIndex correctly forwarded from state.
  • shared/.../presentation/util/SetTypeLabel.kt — see line 26 finding.
  • shared/.../presentation/SetTypeLabelTest.kt — 4 tests covering the main branches; missing a Warmup(setNumber = 2) case.

Reviewed by minimax-m3 · Input: 33.8K · Output: 9.6K · Cached: 294.5K

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a set-type badge (Calibration Reps, Warm-up Set, or Working Set) to the workout screens based on the current set state. The review feedback highlights a logic issue where warm-up sets are incorrectly labeled as calibration reps for the first three reps, suggesting a reordering of conditions and an accompanying unit test. Additionally, the reviewer recommends removing redundant, untranslated English string resources from non-English locale files, as they automatically fall back to the default strings file.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +32 to +39
): SetTypeLabel = when {
showCalibrationLabel && repCount.warmupReps < CALIBRATION_BUFFER_REPS && !repCount.isWarmupComplete ->
SetTypeLabel.Calibration
currentWarmupSetIndex >= 0 ->
SetTypeLabel.Warmup(currentWarmupSetIndex + 1)
else ->
SetTypeLabel.Working
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

During the active set of a warm-up set, the first 3 reps are unweighted calibration reps. However, labeling them as Calibration Reps instead of Warm-up Set N defeats the core purpose of this feature (which is to remind the user that they are in a warm-up set). If a warm-up set is configured with 3 or fewer reps, the user will never see the warm-up label during the active set.

By reordering the conditions so that currentWarmupSetIndex >= 0 takes precedence, we ensure that warm-up sets are always clearly identified as such from the very first rep, while working sets still correctly display Calibration Reps for the first 3 reps.

Suggested change
): SetTypeLabel = when {
showCalibrationLabel && repCount.warmupReps < CALIBRATION_BUFFER_REPS && !repCount.isWarmupComplete ->
SetTypeLabel.Calibration
currentWarmupSetIndex >= 0 ->
SetTypeLabel.Warmup(currentWarmupSetIndex + 1)
else ->
SetTypeLabel.Working
}
): SetTypeLabel = when {
currentWarmupSetIndex >= 0 ->
SetTypeLabel.Warmup(currentWarmupSetIndex + 1)
showCalibrationLabel && repCount.warmupReps < CALIBRATION_BUFFER_REPS && !repCount.isWarmupComplete ->
SetTypeLabel.Calibration
else ->
SetTypeLabel.Working
}

Comment on lines +35 to +41

@Test
fun working_when_no_warmup_index_at_setready() {
val rc = RepCount(warmupReps = 0, isWarmupComplete = false)
val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = false)
assertEquals(SetTypeLabel.Working, label)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add a unit test to verify and pin the precedence behavior where warm-up sets take precedence over calibration reps during the active set.

Suggested change
@Test
fun working_when_no_warmup_index_at_setready() {
val rc = RepCount(warmupReps = 0, isWarmupComplete = false)
val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = false)
assertEquals(SetTypeLabel.Working, label)
}
@Test
fun working_when_no_warmup_index_at_setready() {
val rc = RepCount(warmupReps = 0, isWarmupComplete = false)
val label = setTypeLabel(rc, currentWarmupSetIndex = -1, showCalibrationLabel = false)
assertEquals(SetTypeLabel.Working, label)
}
@Test
fun warmup_takes_precedence_over_calibration_during_active_set() {
val rc = RepCount(warmupReps = 0, isWarmupComplete = false)
val label = setTypeLabel(rc, currentWarmupSetIndex = 0, showCalibrationLabel = true)
assertEquals(SetTypeLabel.Warmup(setNumber = 1), label)
}
}

Comment on lines +703 to +706
<!-- ==================== Set type labels (Issue #646) ==================== -->
<string name="set_type_calibration">Calibration Reps</string>
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Android and Compose Multiplatform, string resources automatically fall back to the default values/strings.xml if they are missing from locale-specific files. Explicitly adding untranslated English strings to non-English locale files is redundant, pollutes the translation files, and makes it harder for translation tools/translators to identify untranslated keys. These should be removed and only added when actual German translations are available.

Comment on lines +703 to +706
<!-- ==================== Set type labels (Issue #646) ==================== -->
<string name="set_type_calibration">Calibration Reps</string>
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Android and Compose Multiplatform, string resources automatically fall back to the default values/strings.xml if they are missing from locale-specific files. Explicitly adding untranslated English strings to non-English locale files is redundant, pollutes the translation files, and makes it harder for translation tools/translators to identify untranslated keys. These should be removed and only added when actual Spanish translations are available.

Comment on lines +703 to +706
<!-- ==================== Set type labels (Issue #646) ==================== -->
<string name="set_type_calibration">Calibration Reps</string>
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Android and Compose Multiplatform, string resources automatically fall back to the default values/strings.xml if they are missing from locale-specific files. Explicitly adding untranslated English strings to non-English locale files is redundant, pollutes the translation files, and makes it harder for translation tools/translators to identify untranslated keys. These should be removed and only added when actual French translations are available.

Comment on lines +70 to +73
<!-- ==================== Set type labels (Issue #646) ==================== -->
<string name="set_type_calibration">Calibration Reps</string>
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Android and Compose Multiplatform, string resources automatically fall back to the default values/strings.xml if they are missing from locale-specific files. Explicitly adding untranslated English strings to non-English locale files is redundant, pollutes the translation files, and makes it harder for translation tools/translators to identify untranslated keys. These should be removed and only added when actual Italian translations are available.

Comment on lines +682 to +685
<!-- ==================== Set type labels (Issue #646) ==================== -->
<string name="set_type_calibration">Calibration Reps</string>
<string name="set_type_warmup">Warm-up Set %1$d</string>
<string name="set_type_working">Working Set</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In Android and Compose Multiplatform, string resources automatically fall back to the default values/strings.xml if they are missing from locale-specific files. Explicitly adding untranslated English strings to non-English locale files is redundant, pollutes the translation files, and makes it harder for translation tools/translators to identify untranslated keys. These should be removed and only added when actual Dutch translations are available.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6955e3afeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

// Issue #646: set-type badge (Calibration Reps / Warm-up Set N / Working Set)
val setType = setTypeLabel(repCount, currentWarmupSetIndex, showCalibrationLabel = true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress calibration labels for bodyweight sets

When a routine reaches a bodyweight exercise while connected, ActiveSessionEngine.startWorkout configures warmupTarget = 0 and stores a fresh RepCount() with isWarmupComplete = false, so this unconditional showCalibrationLabel = true makes setTypeLabel return Calibration before the bodyweight timer branch renders. The HUD then labels a bodyweight set as “Calibration Reps” even though no firmware calibration reps run for bodyweight exercises; pass the exercise type through here or disable the calibration label for isCurrentExerciseBodyweight.

Useful? React with 👍 / 👎.

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

CI + build status

All 10 CI checks pass on this PR — Build Android, Unit Tests, Shared Module Tests, Android App Unit Tests, iOS Test Target Compile, iOS Schema Sync, Lint, Security Hygiene, GitGuardian, Kilo Code Review.

Local verification:

  • ./gradlew :shared:compileCommonMainKotlinMetadata BUILD SUCCESSFUL
  • ./gradlew :shared:compileTestKotlinIosArm64 BUILD SUCCESSFUL
  • ./gradlew :shared:testAndroidHostTest -Pskip.supabase.check=true 2431 tests, 0 failures (incl. the new SetTypeLabelTest's 4 cases)
  • ./gradlew :androidApp:assembleDebug -Pskip.supabase.check=true BUILD SUCCESSFUL
  • Installed androidApp-debug.apk to phoenix_phantom_api36 (API 36) emulator; app launches to "Choose Your Workout" screen successfully — confirms the new composables compile and don't crash on startup.

Visual evidence blocker (heads-up)

The original spec asks for 7 PNGs of the implemented UI on the AVD (01-active-calibration.png etc. through 07-justlift-control.png). I cannot reach those screens from the launcher / production flow without a physically connected Vitruvian + BLE handshake. The flow guards on connectionState is ConnectionState.Connected (WorkoutTab.kt:319) and a real WorkoutState.Active requires BLE-driven rep-count updates to drive repCount.warmupReps/currentWarmupSetIndex.

I considered adding a debug mock-state path that pre-populates ConnectionState.Connected + repCount/currentWarmupSetIndex from a debug menu so the 7 states could be rendered. That is exactly the kind of debug fixture that gets merged and starts fixing "real" bugs by accident — I'd rather not, especially inside a UI-only patch where the production code path is intentionally untouched. The integration model's "Forbidden scope" list explicitly excludes the state machine layers.

The WorkoutTabPreviews.kt Android Compose previews (lines 75, 321, etc.) already mock ConnectionState.Connected(...) for @Preview rendering, but they're IDE-only and don't render to disk in headless environments.

Available evidence right now:

To unblock final-visual capture the reporter or a tester with access to a real Vitruvian can:

  1. Pull this branch, install on a Pixel with the trainer paired over BLE, run a routine with Warm-up Sets enabled and a separate routine without, and screencap the 7 states listed in implementation-spec.md Packet 4 Visual QA.
  2. Drop the PNGs on this PR; I'll cross-link them here.

Or: if the team wants me to add a debug-only mock-state path for visual capture, give the green light and I'll wire it as a separate PhoenixDebugVisualCaptureActivity gated by BuildConfig.DEBUG so it can never ship to release. That feels out of scope for this UI patch, but it's a 1-file, ~50-line add.

Scope reminder

Per the implementation spec, the bounded diff is:

  • 1 new helper file (SetTypeLabel.kt)
  • 1 new test file (SetTypeLabelTest.kt, 4 cases)
  • 6 locale strings.xml (3 keys each: set_type_calibration, set_type_warmup, set_type_working)
  • Modified call sites only: WorkoutHud.kt (ExecutionPage + helper imports), WorkoutTab.kt (1 new param forward), SetReadyScreen.kt (2 collectAsState + inline label)

Forbidden scope explicitly NOT touched: ActiveSessionEngine.kt, RoutineFlowManager.kt, WorkoutCoordinator.kt, BLE/protocol code, DB schema/migrations, SetSummaryCard.kt, and any non-mobile repo (phoenix-portal).

@9thLevelSoftware 9thLevelSoftware merged commit dc921a9 into main Jul 10, 2026
10 checks passed
@9thLevelSoftware 9thLevelSoftware deleted the feat/issue-646-set-type-labels branch July 10, 2026 19:16
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.

Label set types in workout UI: Calibration Reps, Warm-up Sets, Working Sets

1 participant