Skip to content

Add next-set weight recommendation suggestions#499

Merged
9thLevelSoftware merged 3 commits into
mainfrom
feat/424-weight-suggestions
Jun 7, 2026
Merged

Add next-set weight recommendation suggestions#499
9thLevelSoftware merged 3 commits into
mainfrom
feat/424-weight-suggestions

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

  • Add a user preference to enable or disable next-set weight suggestions
  • Wire the recommendation use case through DI, view models, and workout session flow
  • Show an apply/dismiss recommendation card on Set Ready when the suggestion matches the current set
  • Clear stale recommendations when workout state, routine flow, or target set changes

Testing

  • Added and updated host/common tests for preference defaults and view model/session wiring
  • Not run (not requested)

Copilot AI review requested due to automatic review settings June 6, 2026 21:44

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@9thLevelSoftware 9thLevelSoftware linked an issue Jun 6, 2026 that may be closed by this pull request
2 tasks

@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 conservative, suggestion-only next-set weight recommendation feature (Issue #424). It adds the RecommendWeightAdjustmentUseCase to compute weight adjustments based on completed-set quality and biomechanics data, alongside a WeightRecommendationCard UI component on the SetReadyScreen to apply or dismiss suggestions. The feature is integrated into the session management flow and includes a toggle in the settings. Feedback on the changes points out a logical gap where missing target reps by a large margin with high quality still recommends maintaining weight, a missing edge case check for zero completed reps, and several instances of hardcoded user-facing strings that violate internationalization (i18n) standards.

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 +73 to +78
return maintain(
input = input,
reasonCode = "QUALITY_MIXED_MAINTAIN",
explanation = "Keep this weight - rep quality was mixed.",
confidence = if (completedTarget) RecommendationConfidence.MEDIUM else RecommendationConfidence.LOW,
)

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

There is a logical gap when a user fails to complete their target reps by a significant margin (e.g., completing only 1 or 2 reps out of a 10-rep target), but those few reps are of very high quality (e.g., averageScore >= MIXED_QUALITY_MIN).

Currently, this scenario falls through to line 73 and recommends MAINTAIN with QUALITY_MIXED_MAINTAIN (with LOW confidence). Recommending to keep the same weight when the user missed their target reps by 80-90% is counter-intuitive and could lead to injury or overtraining.

Suggestion:
Consider adding a check to ensure that if the target was missed by a significant margin (e.g., actualReps < targetReps * 0.5 or a similar threshold), the engine either recommends a DECREASE or suppresses the recommendation entirely (return null).

Comment on lines +19 to +22
if (input.isBodyweight || !input.hasNextSetTarget) return null
if (input.currentWeightKgPerCable < Constants.MIN_WEIGHT_KG) return null
if (input.weightIncrementKg <= 0f) return null
if (input.qualitySummary == null && input.biomechanicsSummary == null) return null

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

If the user completes 0 reps (e.g., they started and immediately stopped the set), we do not have any actual performance data to base a recommendation on. Currently, if qualitySummary or biomechanicsSummary are non-null but empty, the code might still proceed.

Suggestion:
Add an early return check for input.actualReps <= 0 to safely ignore sets with no completed reps.

Suggested change
if (input.isBodyweight || !input.hasNextSetTarget) return null
if (input.currentWeightKgPerCable < Constants.MIN_WEIGHT_KG) return null
if (input.weightIncrementKg <= 0f) return null
if (input.qualitySummary == null && input.biomechanicsSummary == null) return null
if (input.isBodyweight || !input.hasNextSetTarget || input.actualReps <= 0) return null
if (input.currentWeightKgPerCable < Constants.MIN_WEIGHT_KG) return null
if (input.weightIncrementKg <= 0f) return null
if (input.qualitySummary == null && input.biomechanicsSummary == null) return null

Comment on lines +48 to +52
val headline = when (recommendation.direction) {
WeightAdjustmentDirection.INCREASE -> "Increase next set"
WeightAdjustmentDirection.DECREASE -> "Reduce next set"
WeightAdjustmentDirection.MAINTAIN -> "Keep next set steady"
}

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

Hardcoded user-facing strings violate the internationalization (i18n) standards of the project. The codebase already utilizes stringResource(Res.string....) for localization (as seen in SetReadyScreen.kt).

Suggestion:
Externalize these strings to the project's resource files and load them dynamically.

(Note: Also apply this to other hardcoded strings in this file, such as "per cable" on line 93, "Dismiss" on line 115, and "Apply" on line 118).

Suggested change
val headline = when (recommendation.direction) {
WeightAdjustmentDirection.INCREASE -> "Increase next set"
WeightAdjustmentDirection.DECREASE -> "Reduce next set"
WeightAdjustmentDirection.MAINTAIN -> "Keep next set steady"
}
val headline = when (recommendation.direction) {
WeightAdjustmentDirection.INCREASE -> stringResource(Res.string.weight_recommendation_increase)
WeightAdjustmentDirection.DECREASE -> stringResource(Res.string.weight_recommendation_decrease)
WeightAdjustmentDirection.MAINTAIN -> stringResource(Res.string.weight_recommendation_maintain)
}

Comment on lines +1224 to +1235
Text(
"Weight Suggestions",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Show Apply or Dismiss recommendations after completed sets",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)

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

The newly added settings labels are hardcoded, which violates the project's internationalization (i18n) standards.

Suggestion:
Use stringResource to load these strings from the localized resources.

Suggested change
Text(
"Weight Suggestions",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
"Show Apply or Dismiss recommendations after completed sets",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
stringResource(Res.string.settings_weight_suggestions_title),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
stringResource(Res.string.settings_weight_suggestions_subtitle),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)

@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: 4af4412a22

ℹ️ 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".

targetSetIndex = nextStep.second,
targetReps = params.reps,
actualReps = completedReps,
currentWeightKgPerCable = params.weightPerCableKg,

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 Use the target set weight for recommendations

When a routine programs different weights per set, this builds the recommendation from the completed set's params.weightPerCableKg even though the recommendation targets nextStep. The rest/manual transition code loads the next set's planned setWeightsPerCableKg, so applying a high-quality set-1 recommendation can overwrite a planned heavier set 2 (for example 20kg -> planned 30kg becomes 22.5kg). Please base the suggestion on the target set's planned weight or suppress it when the next set already has a distinct programmed weight.

Useful? React with 👍 / 👎.


operator fun invoke(input: WeightAdjustmentInput): WeightAdjustmentRecommendation? {
if (input.isBodyweight || !input.hasNextSetTarget) return null
if (input.currentWeightKgPerCable < Constants.MIN_WEIGHT_KG) return null

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: This early return check < Constants.MIN_WEIGHT_KG is a masterclass in writing code that looks defensive but does absolutely nothing. With MIN_WEIGHT_KG = 0f, this condition weight < 0 will never be true for any legitimate cable exercise weight. It's like a bouncer checking if you're under -5 feet tall.

🩹 The Fix: Either remove this redundant check, or if you want to catch unconfigured/edge-case weights, change it to <= 0f to catch actual problematic values. The constant should probably be MIN_PRACTICAL_WEIGHT_KG = 0.5f (machine's minimum increment) rather than 0.

📏 Severity: suggestion

}

if (quality != null) {
val finalRepScore = quality.repScores.maxByOrNull { it.repNumber }?.composite

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: maxByOrNull { it.repNumber } to find the "final rep" is clever but reads like you're finding the rep with the maximum number (which happens to be the last one if they're sequential). Future maintainers will need a treasure map to understand this logic.

🩹 The Fix: Add a comment clarifying this finds the last/highest-numbered rep, or use repScores.maxByOrNull { it.repNumber } with an explanatory comment: // Last rep has highest repNumber.

📏 Severity: nitpick


// Bodyweight = no cable accessories (handles, bar, rope, etc.) in equipment list
val isBodyweight = !currentExercise.exercise.hasCableAccessory
val matchingWeightRecommendation = weightRecommendation?.takeIf { recommendation ->

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: The takeIf predicate is checking !isBodyweight but you already have that context from line 120. While not technically wrong, this redundant check adds mental overhead - like checking if a fish has lungs before giving it water.

🩹 The Fix: The check is defensive and ensures the recommendation is appropriate even if state changes. It's acceptable, but consider removing !isBodyweight since isBodyweight is already computed and the recommendation shouldn't target bodyweight exercises anyway (filtered in updateWeightRecommendationForCompletedSet).

📏 Severity: nitpick

@kilo-code-bot

kilo-code-bot Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No New Issues | Recommendation: Merge when ready (address carried-forward issues)

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0

🏆 Best part: This feature addition is impressively thorough - new use case, state management, UI component, settings integration, and comprehensive test coverage including integration tests. The clearWeightRecommendationIfNotTarget helper is a nice touch to prevent stale recommendations across exercise navigation.

💀 Previous issues still outstanding (carry-forward from initial review):

  • RecommendWeightAdjustmentUseCase.kt:20 - The currentWeight < MIN_WEIGHT_KG check with MIN_WEIGHT_KG = 0f is security theater; it catches exactly no realistic negative weight scenarios
  • RecommendWeightAdjustmentUseCase.kt:50 - Using maxByOrNull { it.repNumber } to find the final rep is clever but unintuitive; last() with sortedBy would be clearer
  • SetReadyScreen.kt:121 - The !isBodyweight check in takeIf is redundant since recommendations are never generated for bodyweight exercises

📊 Overall: Like a well-planned workout program - the progression is logical, the form is solid, and someone already pointed out where the previous reps were sloppy. Fix the carried-forward issues when you get a chance, but this PR is merge-worthy.

Files Reviewed (12 files)
  • RecommendWeightAdjustmentUseCase.kt - 2 carried-forward issues
  • SetReadyScreen.kt - 1 carried-forward issue
  • WeightRecommendationCard.kt - clean (new)
  • SettingsManager.kt - clean (new preference wireup)
  • WorkoutCoordinator.kt - clean (new state field)
  • RoutineFlowManager.kt - clean (recommendation clearing)
  • ActiveSessionEngine.kt - clean (recommendation logic)
  • MainViewModel.kt - clean (view model wiring)
  • UserPreferences.kt - clean (new field)
  • WeightRecommendation.kt - clean (new model)
  • Test files - clean

Reviewed by laguna-m.1-20260312:free · 1,106,893 tokens

@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: 651ce3eb11

ℹ️ 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".

exerciseName = targetExercise?.exercise?.name ?: currentExercise?.exercise?.name,
targetExerciseId = targetExerciseId,
targetSetIndex = nextStep.second,
targetReps = params.reps,

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 Use the next set's rep target for suggestions

When a routine programs different reps per set, this still evaluates the recommendation against the completed set's params.reps instead of the target set's configured reps. For example, after completing a high-quality 5-rep set before a planned 10-rep set, the card can recommend increasing the next set's weight even though the next target is much harder; if the next set is AMRAP (setReps[...] == null), it can still show a target-based suggestion. Please read the target set reps from targetExercise.setReps.getOrNull(nextStep.second) and suppress recommendations when that target is absent.

Useful? React with 👍 / 👎.

Both branches independently added new dependencies to the same constructor
parameter lists (RecommendWeightAdjustmentUseCase from the PR branch,
EquipmentRackRepository + ApplyEquipmentRackLoadUseCase from main).
Resolution keeps both features intact across all 11 conflicted files.

https://claude.ai/code/session_018ubmbkr2vYycJueywZnoC2
@9thLevelSoftware 9thLevelSoftware merged commit 8180489 into main Jun 7, 2026
9 checks passed
@9thLevelSoftware 9thLevelSoftware deleted the feat/424-weight-suggestions branch June 7, 2026 23:08
9thLevelSoftware pushed a commit that referenced this pull request Jun 7, 2026
Resolve merge conflicts with weight recommendation (PR #499),
timed-set countdown cues (PR #505), and Health Connect export (PR #516)
by keeping both deload modifiers and the new main features.

https://claude.ai/code/session_01MBWwVaUrnXjzZxYKKnn33i
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.

[Feature]: Suggest Weight Increase

3 participants