Skip to content

feat: implement supersets#136

Merged
isotronic merged 14 commits into
masterfrom
super-sets
May 12, 2026
Merged

feat: implement supersets#136
isotronic merged 14 commits into
masterfrom
super-sets

Conversation

@isotronic
Copy link
Copy Markdown
Owner

@isotronic isotronic commented May 11, 2026

Summary by Sourcery

Add support for creating, editing, and performing supersets across workouts, including persistence and UI indicators.

New Features:

  • Allow users to pair exercises into supersets from the plan editor and manage them via workout cards and exercise selectors.
  • Show visual grouping and labeling of supersets in workout details, workout overview, workout sessions, and set overviews.
  • Introduce superset-aware navigation and flow during active workouts, including automatic transitions between paired exercises without rest.

Enhancements:

  • Keep superset partner sets in sync when adding or removing sets and preserve adjacency when reordering exercises in workouts.
  • Display clearer numbering/labels for exercises participating in supersets (e.g., A/B) and add contextual banners describing superset partners.
  • Harden App Check debug token parsing to ignore non-string values.

Build:

  • Bump app version and Android versionCode for the new release.

Documentation:

  • Add a Whats New entry announcing supersets and the updated weekly schedule features.

Summary by CodeRabbit

  • New Features

    • Supersets: pair two exercises with synced sets, visual grouping (banners/connectors), A/B labels, tailored session progression, creation/removal flows, and preserved adjacency during reorder.
  • Chores

    • App version bumped to 0.21.05.
    • Persistent storage and migrations updated to include superset metadata.
    • Added WhatsNew entry announcing Supersets.

Review Change Stack

isotronic added 4 commits May 11, 2026 19:24
- Added superset handling in WorkoutSessionScreen to manage exercise transitions without rest.
- Enhanced WorkoutCard to allow creation and removal of supersets for exercises.
- Updated WorkoutDetailsScreen to visually indicate superset status for exercises.
- Modified usePlanQuery to include supersetGroupId in workout records.
- Extended activeWorkoutStore to manage superset logic during workout progression.
- Implemented superset management in workoutStore for adding and removing exercises.
- Updated database schema to support supersetGroupId in user_workout_exercises.
- Ensured user data initialization includes supersetGroupId column if not present.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 11, 2026

Reviewer's Guide

Implements superset functionality across workout planning, execution, and persistence: exercises can be paired as supersets with shared set structures, visually grouped in the UI, reordered safely, and stored/loaded via a new superset_group_id field in the database and plan APIs.

File-Level Changes

Change Details Files
Add superset pairing UX and behaviors in workout planning lists and details, including creation/removal and drag-and-drop handling.
  • Introduce handleCreateSuperset and handleRemoveSuperset in WorkoutCard to route into exercise selection with superset context and to clear superset membership via the workout store.
  • Render visual superset grouping in WorkoutCard and WorkoutDetailsScreen (header label, connector, bordered items) based on supersetGroupId pairing between adjacent exercises.
  • Extend drag-and-drop reordering logic in WorkoutCard to keep superset partners adjacent, move both members when dragging within the pair, and prevent non-superset items from splitting a superset.
components/WorkoutCard.tsx
components/WorkoutDetailsScreen.tsx
Add superset-aware workout execution UI and progression logic, including A/B labels and set navigation.
  • Decorate workout overview cards to show superset grouping with left bars, headers, connectors and A/B labels instead of numeric indices for paired exercises.
  • In workout-session, derive superset context for the current exercise, show a superset banner describing partner ordering, and change set-completion flow so first superset exercises skip rest and immediately advance to the partner.
  • Ensure rest timing and navigation use the existing nextSet logic while respecting supersets.
app/(app)/(workout)/index.tsx
app/(app)/(workout)/workout-session.tsx
Update workout store to model supersets, keep sets in sync between paired exercises, and expose superset actions for plan editing.
  • Extend UserExercise with an optional supersetGroupId field and add createSuperset/removeFromSuperset actions that assign a shared random UUID group and clear it when removing.
  • Make addSetToExercise and removeSetFromExercise superset-aware by mirroring set additions/removals to the partner exercise within the same superset group.
  • Wire createSuperset into the create-plan exercises screen, add superset selection mode via route params, and adjust selection behavior and footer CTA visibility when in superset mode.
store/workoutStore.ts
app/(app)/(create-plan)/exercises.tsx
Add superset-aware progression in activeWorkoutStore to handle set indices, carry-over values, and navigation through supersets.
  • Introduce a buildNextSetValues helper that centralizes next-set carry-over logic (weight/reps/time) for a given exercise and set pair, reusing previousWorkoutData when available.
  • When the current exercise is the first in a superset, immediately switch to its partner without rest; when it is the second, advance both exercises’ set indices, pre-fill their next sets, and then find the next uncompleted exercise after the pair.
  • Fallback to the existing non-superset progression path when no supersetGroupId is present, with some comments removed for clarity.
store/activeWorkoutStore.ts
Propagate superset metadata through sets overview, plan queries, and database persistence, plus schema migration.
  • Show superset pairing context in the sets overview screen via a banner indicating the paired exercise name.
  • Add supersetGroupId to WorkoutRecord and RawWorkoutRecord, read/write superset_group_id in all relevant SQL queries and insert/update paths, and deserialize it into in-memory workout/exercise structures.
  • Add a migration in initUserDataDB to append superset_group_id to user_workout_exercises if missing, ensuring backward compatibility of existing user data.
app/(app)/(create-plan)/sets-overview.tsx
hooks/usePlanQuery.ts
utils/database.ts
utils/initUserDataDB.ts
Update app metadata and minor robustness improvement for App Check config.
  • Bump app version and Android versionCode for the new release containing supersets.
  • Guard App Check debug token extraction by enforcing string type before use to avoid misconfigured extra values.
app.config.js
utils/initAppCheck.ts
constants/WhatsNew.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4c663b84-0b18-4d3c-99fb-23aee7610eba

📥 Commits

Reviewing files that changed from the base of the PR and between 4eff251 and f023849.

📒 Files selected for processing (2)
  • app/(app)/(workout)/index.tsx
  • utils/supersetUtils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/(app)/(workout)/index.tsx

📝 Walkthrough

Walkthrough

Adds superset pairing: exercises can be grouped in pairs with synchronized sets. Changes touch data types, DB migration and persistence, store actions and mirroring, selection and list UIs, session progression, utilities for classification, and release metadata.

Changes

Superset Feature Implementation

Layer / File(s) Summary
Data Model & Interfaces
store/workoutStore.ts, hooks/usePlanQuery.ts, hooks/useActivePlanQuery.ts, hooks/useAllPlansQuery.ts
UserExercise gains optional supersetGroupId. Plan/active/all-plans queries select/map superset_group_id.
Database Schema & Persistence
utils/initUserDataDB.ts, utils/database.ts
Migration adds superset_group_id TEXT. All insert/update/read paths persist and return superset_group_id for workouts and standalone flows.
Superset utilities & validation
utils/supersetUtils.ts
Added partner lookup, position classifier, and cardinality validator used by UI and store logic.
Workout Store API & Set Mirroring
store/workoutStore.ts
New createSuperset / removeFromSuperset. addSetToExercise and removeSetFromExercise mirror changes to superset partners; UserExercise extended.
Active Session Progression
store/activeWorkoutStore.ts
nextSet handles superset flow: first member switches to partner without starting rest; second member advances both exercises and applies carry-over next-set values.
Exercise Selection & Superset Creation
app/(app)/(create-plan)/exercises.tsx
Parse supersetForIndex param, enable superset mode, call createSuperset when selecting a partner, and hide bottom action buttons in superset mode.
WorkoutCard Grouping & Reordering
components/WorkoutCard.tsx
Group exercises into sortable single/pair units, add create/remove superset menu actions, and reorder grouped units then flatten back to workout.exercises.
Workout List, Details & Sets Overview
app/(app)/(workout)/index.tsx, components/WorkoutDetailsScreen.tsx, app/(app)/(create-plan)/sets-overview.tsx
Render "Superset" header/connector, show A/B markers, adjust press behaviour for second member, show superset banners and partner names, and add superset-specific styles.
Workout Session UI
app/(app)/(workout)/workout-session.tsx
Session UI shows Superset A/B banners and adapts complete-set handling for superset semantics.
Release & Misc
app.config.js, constants/WhatsNew.ts, utils/initAppCheck.ts
Bump app version to 0.21.05 and Android versionCode to 2105; add WhatsNew entry for version 2609; type-check appCheck debug token before use.

Sequence Diagram

sequenceDiagram
  participant User
  participant ExercisesScreen
  participant WorkoutStore
  participant Database
  participant WorkoutSessionScreen
  User->>ExercisesScreen: Open in superset mode (supersetForIndex)
  User->>ExercisesScreen: Select partner exercise
  ExercisesScreen->>WorkoutStore: createSuperset(workoutIndex, exerciseIndex, newExercise)
  WorkoutStore->>WorkoutStore: generate supersetGroupId and insert partner
  WorkoutStore->>Database: insert/update exercises with superset_group_id
  Database-->>WorkoutStore: confirm persistence
  ExercisesScreen->>User: navigate back
  User->>WorkoutSessionScreen: complete set on first superset member
  WorkoutSessionScreen->>WorkoutStore: nextSet(currentIndex)
  WorkoutStore->>WorkoutStore: detect supersetGroupId -> switch to partner (no rest)
  WorkoutStore-->>WorkoutSessionScreen: updated indices/values
  User->>WorkoutSessionScreen: complete set on second member
  WorkoutSessionScreen->>WorkoutStore: nextSet(partnerIndex)
  WorkoutStore->>WorkoutStore: advance both exercises, compute carry-over
  WorkoutStore-->>WorkoutSessionScreen: updated indices or finish workout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Two bunnies pair, A then B,

They mirror sets in harmony.
Cards show A/B and banners cheer,
Drag keeps the pair close and near.
Superset snug — a joyful cheer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement supersets' directly and clearly describes the primary feature added in this PR: comprehensive superset functionality across the entire application.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The superset partner detection logic (finding partnerIndex, isFirstInSuperset, isSecondInSuperset) is duplicated across multiple components (WorkoutCard, WorkoutOverviewScreen, WorkoutDetailsScreen, WorkoutSession, etc.); consider extracting a shared helper to keep the behavior consistent and reduce future maintenance overhead.
  • Superset-specific UI elements (headers, connector bars, left borders, etc.) have very similar style definitions in multiple screens; pulling these into a shared component or style module would reduce duplication and make it easier to tweak the visual design in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The superset partner detection logic (finding `partnerIndex`, `isFirstInSuperset`, `isSecondInSuperset`) is duplicated across multiple components (WorkoutCard, WorkoutOverviewScreen, WorkoutDetailsScreen, WorkoutSession, etc.); consider extracting a shared helper to keep the behavior consistent and reduce future maintenance overhead.
- Superset-specific UI elements (headers, connector bars, left borders, etc.) have very similar style definitions in multiple screens; pulling these into a shared component or style module would reduce duplication and make it easier to tweak the visual design in one place.

## Individual Comments

### Comment 1
<location path="components/WorkoutCard.tsx" line_range="158" />
<code_context>
           const isLoading = loadingExerciseIndex === index;
+
+          const { supersetGroupId } = exercise;
+          const partnerIndex = supersetGroupId
+            ? workout.exercises.findIndex(
+                (e, i) => i !== index && e.supersetGroupId === supersetGroupId,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the superset partner and reordering logic into shared pure helper functions to simplify the component and centralize the rules.

You can reduce the added complexity by extracting the superset logic into small, pure helpers and reusing them in both `renderExerciseItem` and `handleOrderChange`. That keeps the component code as orchestration and makes the index juggling easier to test and reason about.

### 1. Extract superset helpers

Create a small set of utilities (ideally in a shared file used by WorkoutOverview/WorkoutDetails too) that centralize the “partner” and “superset adjacency” logic:

```ts
// supersetUtils.ts (or nearby file)

export function findSupersetPartnerIndex(
  exercises: UserExercise[],
  index: number,
): number {
  const supersetGroupId = exercises[index]?.supersetGroupId;
  if (!supersetGroupId) return -1;

  return exercises.findIndex(
    (e, i) => i !== index && e.supersetGroupId === supersetGroupId,
  );
}

export function classifySupersetPosition(
  exercises: UserExercise[],
  index: number,
): {
  isInSuperset: boolean;
  partnerIndex: number;
  isFirstInSuperset: boolean;
  isSecondInSuperset: boolean;
} {
  const partnerIndex = findSupersetPartnerIndex(exercises, index);
  const isInSuperset = partnerIndex !== -1;

  return {
    isInSuperset,
    partnerIndex,
    isFirstInSuperset: isInSuperset && index < partnerIndex,
    isSecondInSuperset: isInSuperset && index > partnerIndex,
  };
}
```

Then `renderExerciseItem` becomes simpler and avoids duplicating the partner-finding logic:

```ts
const renderExerciseItem: SortableGridRenderItem<UserExercise> = useCallback(
  ({ item, index: exerciseIndex }) => {
    // ...
    const {
      isInSuperset,
      partnerIndex,
      isFirstInSuperset,
      isSecondInSuperset,
    } = classifySupersetPosition(workout.exercises, exerciseIndex);

    // use isInSuperset, isFirstInSuperset, isSecondInSuperset as before
  },
  [
    handleCreateSuperset,
    handleRemoveSuperset,
    handleReplace,
    menuVisible,
    removeExercise,
    workout.exercises,
    workoutIndex,
  ],
);
```

This removes the inline `findIndex` plus boolean derivation, and gives a single place to update if superset rules change.

### 2. Extract superset-aware reordering

You can also pull the core “drag superset / don’t break superset” logic out of `handleOrderChange` into a pure helper that operates on arrays:

```ts
// supersetUtils.ts (continued)

export function reorderWithSupersetRules(
  exercises: UserExercise[],
  fromIndex: number,
  toIndex: number,
): UserExercise[] {
  if (fromIndex === toIndex) return exercises;

  const updated = [...exercises];
  const draggedExercise = updated[fromIndex];
  const supersetGroupId = draggedExercise.supersetGroupId;

  // Generic move
  const [movedItem] = updated.splice(fromIndex, 1);
  updated.splice(toIndex, 0, movedItem);

  if (supersetGroupId) {
    const originalPartnerIndex = findSupersetPartnerIndex(exercises, fromIndex);
    const isDraggingFirst =
      originalPartnerIndex !== -1 && fromIndex < originalPartnerIndex;

    const draggedNewIdx = updated.findIndex(
      (e) => e.exercise_id === draggedExercise.exercise_id,
    );
    const partnerNewIdx = updated.findIndex(
      (e) =>
        e.exercise_id !== draggedExercise.exercise_id &&
        e.supersetGroupId === supersetGroupId,
    );

    if (partnerNewIdx !== -1 && Math.abs(draggedNewIdx - partnerNewIdx) !== 1) {
      const [partner] = updated.splice(partnerNewIdx, 1);
      const newDraggedIdx = updated.findIndex(
        (e) => e.exercise_id === draggedExercise.exercise_id,
      );
      updated.splice(
        isDraggingFirst ? newDraggedIdx + 1 : newDraggedIdx,
        0,
        partner,
      );
    }
  } else {
    const landedIdx = updated.findIndex(
      (e) => e.exercise_id === draggedExercise.exercise_id,
    );
    const prevItem = landedIdx > 0 ? updated[landedIdx - 1] : null;
    const nextItem =
      landedIdx < updated.length - 1 ? updated[landedIdx + 1] : null;

    if (
      prevItem?.supersetGroupId &&
      nextItem?.supersetGroupId &&
      prevItem.supersetGroupId === nextItem.supersetGroupId
    ) {
      updated.splice(landedIdx, 1);
      const secondPartnerIdx = updated.findIndex(
        (e) => e.exercise_id === nextItem.exercise_id,
      );
      updated.splice(secondPartnerIdx + 1, 0, draggedExercise);
    }
  }

  return updated;
}
```

Now `handleOrderChange` becomes a thin wrapper:

```ts
const handleOrderChange = useCallback(
  ({ fromIndex, toIndex }: { fromIndex: number; toIndex: number }) => {
    const updatedExercises = reorderWithSupersetRules(
      workout.exercises,
      fromIndex,
      toIndex,
    );

    const updatedWorkouts = workouts.map((w, i) =>
      i === index ? { ...w, exercises: updatedExercises } : w,
    );

    useWorkoutStore.setState({ workouts: updatedWorkouts });
  },
  [workout.exercises, workouts, index],
);
```

This preserves your current behavior but:

- Removes index arithmetic and `findIndex` branching from the component.
- Centralizes superset invariants in one place.
- Makes the core superset behaviors unit-testable in isolation (`reorderWithSupersetRules`, `classifySupersetPosition`, `findSupersetPartnerIndex`), which should ease future changes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread components/WorkoutCard.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
store/workoutStore.ts (2)

231-265: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

addSetToExercise mirror copies the partner's last set, which can drift from the target set.

When mirroring to the partner you copy partner.sets[partner.sets.length - 1] rather than newSet. If the user has just edited the target's latest set (different rest, warmup flag, reps range, etc.), the partner ends up with the partner's previous template instead of matching newSet. That's almost certainly not what the user expects from a "synchronised" superset.

Consider mirroring newSet itself (or at minimum its rest/warmup metadata) to the partner so additions stay symmetric:

♻️ Suggested change
-          // Mirror to superset partner: copy partner's last set as template
+          // Mirror to superset partner using the same set definition
           if (partner && exercise.exercise_id === partner.exercise_id) {
-            const lastSet = exercise.sets[exercise.sets.length - 1] ?? newSet;
-            return { ...exercise, sets: [...exercise.sets, { ...lastSet }] };
+            return { ...exercise, sets: [...exercise.sets, { ...newSet }] };
           }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/workoutStore.ts` around lines 231 - 265, In addSetToExercise, the
superset partner is currently appended with the partner's previous lastSet which
can diverge from the newly added set; instead, when partner exists, append a
shallow copy of newSet (or at minimum copy its rest/warmup/metadata fields) to
the partner's sets so the partner stays synchronized; update the mapping inside
addSetToExercise to use newSet (e.g., {...newSet} or a minimal metadata object
copied from newSet) when exercise.exercise_id === partner.exercise_id rather
than using the partner's last set.

300-351: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Superset partners can drift out of sync when initial set counts differ.

createSuperset inserts newExercise with its own sets array (from defaultSets/defaultTimeSets in the picker) while keeping the existing exercise's sets untouched. Once paired, addSetToExercise appends one set to each partner and removeSetFromExercise removes the same setIndex from both — both of which only stay coherent if the two arrays start out the same length.

If the original exercise already has more sets than the default (e.g. a user added extra sets before creating the superset), the partner stays one or more sets short forever, and removeSetFromExercise will silently no‑op on the partner whenever setIndex >= partner.sets.length. The active session's nextSet logic in store/activeWorkoutStore.ts (lines 286-316) also assumes both exercises share nextSetIndex < currentExercise.sets.length, so divergence will cause one partner's set to be skipped or the superset to "finish" prematurely.

Suggest aligning set counts at superset creation time so both partners always agree. Mirroring the target exercise's sets onto the new partner (preserving the partner's tracking_type-appropriate defaults for time vs reps) is a low‑risk fix:

♻️ Suggested fix in createSuperset
   createSuperset: (workoutIndex, exerciseIndex, newExercise) =>
     set((state) => {
       const groupId = Crypto.randomUUID();
       const updatedWorkouts = state.workouts.map((w, i) => {
         if (i !== workoutIndex) return w;
+        const target = w.exercises[exerciseIndex];
+        if (!target) return w;
         const exercises = [...w.exercises];
         exercises[exerciseIndex] = {
-          ...exercises[exerciseIndex],
+          ...target,
           supersetGroupId: groupId,
         };
+        // Align partner's set count to the target so additions/removals stay paired.
+        const alignedSets = target.sets.map(() =>
+          newExercise.sets[0]
+            ? { ...newExercise.sets[0] }
+            : ({} as (typeof newExercise.sets)[number]),
+        );
         exercises.splice(exerciseIndex + 1, 0, {
           ...newExercise,
+          sets: alignedSets,
           supersetGroupId: groupId,
         });
         return { ...w, exercises };
       });
       return { workouts: updatedWorkouts };
     }),

Also applies to: 352-369

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/workoutStore.ts` around lines 300 - 351, removeSetFromExercise can
leave superset partners out of sync because createSuperset inserts a newExercise
with its own default sets, so instruct createSuperset to align the partner's
sets length to the target exercise's sets length: when creating the superset
(createSuperset), compute the target exercise's number of sets and fill the new
partner's sets by generating that many entries using the partner's tracking_type
with defaultSets or defaultTimeSets as appropriate (preserve each generated set
shape but not copy values), and also harden removeSetFromExercise and
addSetToExercise to guard against out‑of‑range indices by checking setIndex <
exercise.sets.length before filtering/mirroring (use symbols supersetGroupId,
partner, removeSetFromExercise, addSetToExercise, defaultSets, defaultTimeSets,
createSuperset).
🧹 Nitpick comments (1)
store/activeWorkoutStore.ts (1)

205-259: ⚡ Quick win

Nice extraction of buildNextSetValues.

Pulling the warmup/drop-set + historical-carryover computation into a single helper keeps the superset branch readable and avoids duplicating the tracking-type fan-out. Consider following up by routing the non-superset path (lines 375-426) through the same helper to remove the remaining duplicate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/activeWorkoutStore.ts` around lines 205 - 259, The non-superset branch
still duplicates the warmup/drop-set and historical carryover logic—refactor it
to call the extracted helper buildNextSetValues instead of reimplementing the
same logic: locate the non-superset code that currently computes
weight/reps/time for a next set (the block around the other branch that uses
weightAndReps, previousWorkoutData, and formatFromTotalSeconds) and replace its
inline computation with a call to buildNextSetValues(exerciseIndex, exercise,
fromSetIndex, toSetIndex) (or appropriate indices used there), then use the
returned weight/reps/time values; ensure the helper’s dependencies
(exTrackingType, weightAndReps, previousWorkoutData, formatFromTotalSeconds,
workout.exercises) are in scope or passed through if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/(workout)/index.tsx:
- Around line 490-512: The tap handler currently allows starting a superset by
tapping the "B" row which triggers the second-in-superset path; change the
TouchableOpacity onPress to guard using the computed isSecondInSuperset and
short-circuit (no-op) when true so taps on the B row do nothing. Locate the
supersetGroupId/partnerIndex/isFirstInSuperset/isSecondInSuperset logic and
update the onPress that calls handleExercisePress(index) to only call it when
isSecondInSuperset is false (i.e., allow tap when not the second member of a
superset).

In `@app/`(app)/(workout)/workout-session.tsx:
- Around line 394-397: When jumping directly to the superset partner in the
isFirstInSuperset branch, first cancel any active rest countdown/notification
before calling nextSet(); call the existing rest cancellation helper (e.g.,
cancelRestCountdown / clearRestTimer / stopRestTimer or the function that clears
the rest timeout and notification) and guard it (check existence) so the timer
and any scheduled notification are cleared, then call nextSet(); if no helper
exists, add a small helper that clears the timeout/notification and invoke it
here to prevent stale timers from firing during the partner exercise.

In `@components/WorkoutCard.tsx`:
- Around line 305-350: The current move-fix only reattaches the moved exercise’s
own superset partner, but can leave some other superset pair split; after
performing the existing reordering (the blocks that use updatedExercises,
draggedExercise, supersetGroupId, isDraggingFirst), run a final pass that scans
updatedExercises for any supersetGroupId groups with exactly two members and
ensures they are adjacent: for each such group, find both indices, and if
Math.abs(idxA-idxB) !== 1 remove the later element and splice it immediately
next to the earlier one (preserving their original relative order or the
same-side rule used above); this guarantees no unrelated superset pair remains
split after a drag.

In `@store/activeWorkoutStore.ts`:
- Around line 286-316: The second-in-superset branch only builds next-set values
for the partner (firstIndex) and never populates weightAndReps for the active
exercise (currentExerciseIndex); call buildNextSetValues for the active exercise
as well (e.g., compute secondNextValues =
buildNextSetValues(currentExerciseIndex, currentExercise, currentSetIndex,
nextSetIndex)) and merge it into updatedWeightAndReps alongside the partner's
entry (preserving existing weightAndReps entries) so both indices ([firstIndex]
and [currentExerciseIndex]) have [nextSetIndex] populated; keep
updatedSetIndices and the return shape the same.
- Around line 309-316: The superset completion check only compares nextSetIndex
to currentExercise.sets.length, which can mis-handle supersets when paired
exercises have differing sets; update the logic in the block using
nextSetIndex/currentExercise/updatedCompletedSets (and related
updatedSetIndices/updatedWeightAndReps) so it either (preferably) ensures sets
are aligned at superset creation in createSuperset (workoutStore.ts) or
defensively computes the superset length here (e.g., use the max of both partner
exercises' sets lengths) and compare nextSetIndex against that, and also
guard/clamp accesses into updatedCompletedSets and other per-exercise arrays so
no index goes out of bounds when partners differ. Ensure references:
nextSetIndex, currentExercise, updatedCompletedSets, updatedSetIndices,
updatedWeightAndReps, and createSuperset.

---

Outside diff comments:
In `@store/workoutStore.ts`:
- Around line 231-265: In addSetToExercise, the superset partner is currently
appended with the partner's previous lastSet which can diverge from the newly
added set; instead, when partner exists, append a shallow copy of newSet (or at
minimum copy its rest/warmup/metadata fields) to the partner's sets so the
partner stays synchronized; update the mapping inside addSetToExercise to use
newSet (e.g., {...newSet} or a minimal metadata object copied from newSet) when
exercise.exercise_id === partner.exercise_id rather than using the partner's
last set.
- Around line 300-351: removeSetFromExercise can leave superset partners out of
sync because createSuperset inserts a newExercise with its own default sets, so
instruct createSuperset to align the partner's sets length to the target
exercise's sets length: when creating the superset (createSuperset), compute the
target exercise's number of sets and fill the new partner's sets by generating
that many entries using the partner's tracking_type with defaultSets or
defaultTimeSets as appropriate (preserve each generated set shape but not copy
values), and also harden removeSetFromExercise and addSetToExercise to guard
against out‑of‑range indices by checking setIndex < exercise.sets.length before
filtering/mirroring (use symbols supersetGroupId, partner,
removeSetFromExercise, addSetToExercise, defaultSets, defaultTimeSets,
createSuperset).

---

Nitpick comments:
In `@store/activeWorkoutStore.ts`:
- Around line 205-259: The non-superset branch still duplicates the
warmup/drop-set and historical carryover logic—refactor it to call the extracted
helper buildNextSetValues instead of reimplementing the same logic: locate the
non-superset code that currently computes weight/reps/time for a next set (the
block around the other branch that uses weightAndReps, previousWorkoutData, and
formatFromTotalSeconds) and replace its inline computation with a call to
buildNextSetValues(exerciseIndex, exercise, fromSetIndex, toSetIndex) (or
appropriate indices used there), then use the returned weight/reps/time values;
ensure the helper’s dependencies (exTrackingType, weightAndReps,
previousWorkoutData, formatFromTotalSeconds, workout.exercises) are in scope or
passed through if needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f2b045f1-60b4-45bf-8bd4-cc888ccaa349

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4623e and dabeb58.

📒 Files selected for processing (14)
  • app.config.js
  • app/(app)/(create-plan)/exercises.tsx
  • app/(app)/(create-plan)/sets-overview.tsx
  • app/(app)/(workout)/index.tsx
  • app/(app)/(workout)/workout-session.tsx
  • components/WorkoutCard.tsx
  • components/WorkoutDetailsScreen.tsx
  • constants/WhatsNew.ts
  • hooks/usePlanQuery.ts
  • store/activeWorkoutStore.ts
  • store/workoutStore.ts
  • utils/database.ts
  • utils/initAppCheck.ts
  • utils/initUserDataDB.ts

Comment thread app/(app)/(workout)/index.tsx Outdated
Comment thread app/(app)/(workout)/workout-session.tsx
Comment thread components/WorkoutCard.tsx Outdated
Comment thread store/activeWorkoutStore.ts
Comment thread store/activeWorkoutStore.ts Outdated
@isotronic
Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@isotronic
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 12, 2026

Sorry @isotronic, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

1 similar comment
@SourceryAI
Copy link
Copy Markdown

Sorry @isotronic, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/supersetUtils.ts`:
- Around line 67-92: reorderWithSupersetRules relies on finding rows by
exercise_id via multiple findIndex calls (draggedNewIdx, partnerNewIdx,
landedIdx), which breaks if exercise_id duplicates exist; fix by either
enforcing uniqueness in the store action addExercise (validate and reject or
dedupe incoming exercises) or switch to stable per-row identifiers (capture and
pass the source index/toIndex or attach a UUID per row and use that for lookups
and Sortable.Grid keyExtractor). Also optimize the common case by using the
provided toIndex (use toIndex as draggedNewIdx) instead of calling findIndex
again in reorderWithSupersetRules and replace other findIndex usages with
index-based lookups or UUID-based lookups to avoid silent wrong-row operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d1fdbb4a-42ab-4dfd-b854-a141d2d5b06b

📥 Commits

Reviewing files that changed from the base of the PR and between e2e4402 and a81c05a.

📒 Files selected for processing (6)
  • app/(app)/(workout)/index.tsx
  • app/(app)/(workout)/workout-session.tsx
  • components/WorkoutCard.tsx
  • components/WorkoutDetailsScreen.tsx
  • store/activeWorkoutStore.ts
  • utils/supersetUtils.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/(app)/(workout)/workout-session.tsx
  • app/(app)/(workout)/index.tsx
  • components/WorkoutDetailsScreen.tsx
  • store/activeWorkoutStore.ts

Comment thread utils/supersetUtils.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
hooks/useActivePlanQuery.ts (1)

79-79: 💤 Low value

Consider simplifying the nullish coalescing.

The ?? undefined operator is redundant since null and undefined already evaluate to undefined by default. You can simplify this to just row.superset_group_id.

♻️ Simplified version
-          supersetGroupId: row.superset_group_id ?? undefined,
+          supersetGroupId: row.superset_group_id,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useActivePlanQuery.ts` at line 79, The object property assignment uses
a redundant nullish coalescing; replace "supersetGroupId: row.superset_group_id
?? undefined" with a direct assignment "supersetGroupId: row.superset_group_id"
in the code that builds the result (look for supersetGroupId and
row.superset_group_id in useActivePlanQuery.ts) so the value is passed through
unchanged without the unnecessary "?? undefined".
app/(app)/(workout)/index.tsx (1)

495-502: ⚡ Quick win

Consider using a conditional wrapper for better accessibility.

Setting onPress to undefined disables the press handler, but TouchableOpacity still renders with touchable visual feedback (opacity changes, etc.), which may confuse users when they tap the "B" exercise card. For better accessibility and user experience, consider conditionally rendering either TouchableOpacity or a plain View based on isSecondInSuperset.

♻️ Improved approach
-              <TouchableOpacity
-                onPress={
-                  isSecondInSuperset
-                    ? undefined
-                    : () => {
-                        handleExercisePress(index);
-                      }
-                }
-              >
+              {isSecondInSuperset ? (
+                <View>
+                  {/* card content */}
+                </View>
+              ) : (
+                <TouchableOpacity
+                  onPress={() => {
+                    handleExercisePress(index);
+                  }}
+                >
+                  {/* card content */}
+                </TouchableOpacity>
+              )}

Note: This would require extracting the card content to avoid duplication, or wrapping the entire View conditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/(workout)/index.tsx around lines 495 - 502, The TouchableOpacity
currently sets onPress to undefined when isSecondInSuperset, which still renders
touch feedback and can confuse users; replace the conditional onPress with a
conditional wrapper: render TouchableOpacity with onPress={() =>
handleExercisePress(index)} when !isSecondInSuperset, and render a
non-interactive View (or Pressable with accessible={false}) with the exact same
child content when isSecondInSuperset. Extract the card JSX into a small
subcomponent or variable (e.g., ExerciseCardContent) and use it inside either
<TouchableOpacity> or <View> so you avoid duplicating markup while preserving
accessibility and visuals.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/supersetUtils.ts`:
- Around line 7-17: The findSupersetPartnerIndex function assumes each
supersetGroupId maps to exactly two exercises but has no documentation or
runtime checks; update the code by adding a clear JSDoc above
findSupersetPartnerIndex that states the cardinality constraint (each
supersetGroupId should appear exactly twice and the function returns the other
index or -1), and add validation where supersetGroupId values are created or
loaded (or within a new helper invoked by data load) to assert/throw if a
supersetGroupId appears fewer or more than 2 times—reference the function name
findSupersetPartnerIndex and the supersetGroupId field when placing these checks
so future maintainers can find and enforce the constraint.

---

Nitpick comments:
In `@app/`(app)/(workout)/index.tsx:
- Around line 495-502: The TouchableOpacity currently sets onPress to undefined
when isSecondInSuperset, which still renders touch feedback and can confuse
users; replace the conditional onPress with a conditional wrapper: render
TouchableOpacity with onPress={() => handleExercisePress(index)} when
!isSecondInSuperset, and render a non-interactive View (or Pressable with
accessible={false}) with the exact same child content when isSecondInSuperset.
Extract the card JSX into a small subcomponent or variable (e.g.,
ExerciseCardContent) and use it inside either <TouchableOpacity> or <View> so
you avoid duplicating markup while preserving accessibility and visuals.

In `@hooks/useActivePlanQuery.ts`:
- Line 79: The object property assignment uses a redundant nullish coalescing;
replace "supersetGroupId: row.superset_group_id ?? undefined" with a direct
assignment "supersetGroupId: row.superset_group_id" in the code that builds the
result (look for supersetGroupId and row.superset_group_id in
useActivePlanQuery.ts) so the value is passed through unchanged without the
unnecessary "?? undefined".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1a439c25-0624-4d35-929e-785d365da134

📥 Commits

Reviewing files that changed from the base of the PR and between a81c05a and 4eff251.

📒 Files selected for processing (6)
  • app/(app)/(workout)/index.tsx
  • components/WorkoutCard.tsx
  • components/WorkoutDetailsScreen.tsx
  • hooks/useActivePlanQuery.ts
  • hooks/useAllPlansQuery.ts
  • utils/supersetUtils.ts
✅ Files skipped from review due to trivial changes (1)
  • hooks/useAllPlansQuery.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/WorkoutDetailsScreen.tsx

Comment thread utils/supersetUtils.ts
@isotronic isotronic merged commit 3acd62f into master May 12, 2026
3 checks passed
@isotronic isotronic deleted the super-sets branch May 12, 2026 17:13
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.

2 participants