Conversation
|
@nanaabdul1172 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
📝 WalkthroughWalkthroughThis PR implements multi-winner milestone-based bounty flow with state persistence to localStorage. It introduces a new Changes
Sequence Diagram(s)sequenceDiagram
participant Contributor
participant UI as Milestone Flow Card
participant Hook as useMilestoneFlow
participant Storage as localStorage
Contributor->>UI: Click "Join Milestone Flow"
UI->>Hook: joinFlow(contributorId, name)
Hook->>Hook: Validate capacity & no duplicate
Hook->>Hook: Create participant at stage 0
Hook->>Storage: Persist state (milestone_flow_*)
Hook-->>UI: Return updated state
UI->>Contributor: Show "ACTIVE" status
Contributor->>UI: Submit milestone with PR URL
UI->>Hook: submitMilestone(contributorId, url)
Hook->>Hook: Create PENDING submission
Hook->>Storage: Persist updated state
Hook-->>UI: Return pending submission
UI->>Contributor: Show "SUBMITTED" status
sequenceDiagram
participant Sponsor
participant UI as Milestone Flow Card
participant Hook as useMilestoneFlow
participant Storage as localStorage
Sponsor->>UI: View pending submissions queue
UI->>Hook: Access pendingSubmissions
Hook-->>UI: Return filtered submissions
UI->>Sponsor: Display review list
Sponsor->>UI: Review submission (APPROVED)
UI->>Hook: reviewSubmission(submissionId, decision)
Hook->>Hook: Update submission with decision
Hook->>Hook: Advance participant to next stage<br/>(or mark COMPLETED if final)
Hook->>Storage: Persist updated state
Hook-->>UI: Return updated participant
UI->>Sponsor: Confirm advancement
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (3 warnings, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@nanaabdul1172 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/bounty-detail/bounty-detail-milestone-flow-card.tsx`:
- Around line 289-358: The sponsor queue currently only calls
handleReview(submission.id, "APPROVED"/"REJECTED") and lacks a per-stage
payout/escrow-release step; add a release/capture flow that runs when a sponsor
approves. Update the UI in bounty-detail-milestone-flow-card to either replace
or supplement the Approve button with an "Approve & Release" action that calls a
new handler (e.g., handleSponsorApprove or extend handleReview to accept a
"RELEASE" action), and implement that handler to: 1) call the backend endpoint
that performs the escrow release/transaction capture (suggested function names:
releaseMilestonePayout, captureTransaction, or markMilestonePaid) using
submission.id and submission.milestoneIndex, 2) update local state.milestones
and pendingSubmissions to reflect paid/audited status, and 3) surface errors via
the existing error/logging flow; keep the Reject path unchanged. Ensure you
reference submission.id, submission.milestoneIndex, pendingSubmissions,
state.milestones, and handleReview when making the changes.
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 38-40: The CTA label change for MILESTONE_BASED bounties is
correct, but the click handlers still open bounty.githubIssueUrl; update the
primary and secondary click handlers in bounty-detail-sidebar-cta.tsx so that
when bounty.type === "MILESTONE_BASED" they invoke the milestone flow handler
instead of navigating to bounty.githubIssueUrl — i.e., replace calls that open
or link to bounty.githubIssueUrl with the existing milestone flow entry point
(e.g., openMilestoneFlow / joinMilestoneFlow / onOpenMilestoneModal) used
elsewhere in the app, and apply this change for all instances noted (the other
CTA handlers in the same file). Ensure the conditional uses bounty.type ===
"MILESTONE_BASED" to select the milestone handler and leaves non-milestone
behavior unchanged.
In `@hooks/use-milestone-flow.ts`:
- Around line 13-45: The hook hardcodes milestone definitions via
createDefaultMilestones and createInitialState so every bounty gets the same
8→4→2 and 30/30/40 payout regardless of real bounty config; change the API to
accept milestone definitions from the caller: update useMilestoneFlow to accept
an optional initialMilestones parameter (or a full config object), modify
createInitialState to accept that milestones array (falling back to
createDefaultMilestones()), and update createDefaultMilestones to support
parameterized overrides if needed; ensure all places constructing the state (and
tests) pass the real milestone defs so UI and capacity checks use the
bounty-specific funnel.
- Around line 64-67: The milestone flow state is stored client-side with
useLocalStorage
(`useLocalStorage<MilestoneFlowState>(\`${FLOW_KEY_PREFIX}${bountyId}\`,
createInitialState(bountyId))`), which makes joins/reviews/submissions
local-only and user-editable; change this to a shared, server-backed state:
replace the useLocalStorage usage for MilestoneFlowState with API-backed
persistence (e.g., load/save via your milestones endpoints using bountyId) and
add real-time sync (websocket/subscribe) so updates to the state are written
through the server and broadcast to other clients; update functions that call
setState to call the server save/update methods and reconcile incoming server
events to update the local state.
- Around line 223-241: Advancement currently consumes next-stage slots on
approval; change it to perform a top-N selection instead: when computing
advancement in use-milestone-flow (around submission.milestoneIndex /
current.milestones and isStageOccupied usage), gather all candidates for
nextMilestone (existing current.participants whose currentMilestoneIndex ===
nextMilestoneIndex plus this submission), sort them by the canonical score field
(e.g., submission.score or participant.score) and permit advancement only if
this submission ranks within nextMilestone.maxWinners; otherwise mark the
submission as waitlisted/review-pending instead of immediately claiming a slot.
Update the slot-check logic that uses occupiedNextStage and set
updatedParticipant.status and currentMilestoneIndex only for selected winners.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a69f2681-e24e-4b2d-9d72-a53196890ab6
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
app/profile/[userId]/page.tsxcomponents/bounty-detail/bounty-detail-milestone-flow-card.tsxcomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty-detail/bounty-detail-submissions-card.tsxhooks/__tests__/use-milestone-flow.test.tshooks/use-milestone-flow.tspackage.jsontypes/milestone-flow.ts
| {isOrgMember && ( | ||
| <div className="p-5 rounded-xl border border-gray-800 bg-background-card space-y-4"> | ||
| <h4 className="text-sm font-semibold text-gray-200"> | ||
| Sponsor Review Queue | ||
| </h4> | ||
|
|
||
| {pendingSubmissions.length === 0 && ( | ||
| <p className="text-xs text-gray-400"> | ||
| No pending milestone submissions. | ||
| </p> | ||
| )} | ||
|
|
||
| {pendingSubmissions.length > 0 && ( | ||
| <div className="space-y-3"> | ||
| {pendingSubmissions.map((submission) => { | ||
| const milestone = state.milestones[submission.milestoneIndex]; | ||
| return ( | ||
| <div | ||
| key={submission.id} | ||
| className="p-3 rounded-lg border border-gray-700 bg-gray-900/30 space-y-2" | ||
| > | ||
| <div className="flex items-start justify-between gap-3"> | ||
| <div className="space-y-1 min-w-0"> | ||
| <p className="text-sm text-gray-200 font-medium"> | ||
| {submission.contributorName} | ||
| </p> | ||
| <p className="text-xs text-gray-400"> | ||
| {milestone?.title} | ||
| </p> | ||
| {isSafeHttpUrl(submission.githubPullRequestUrl) ? ( | ||
| <a | ||
| className="text-xs text-primary hover:underline break-all inline-flex items-center gap-1" | ||
| href={submission.githubPullRequestUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| > | ||
| <GitBranch className="size-3" /> | ||
| {submission.githubPullRequestUrl} | ||
| </a> | ||
| ) : ( | ||
| <span className="text-xs text-gray-500 break-all"> | ||
| {submission.githubPullRequestUrl} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <div className="flex gap-2 shrink-0"> | ||
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| onClick={() => | ||
| handleReview(submission.id, "REJECTED") | ||
| } | ||
| > | ||
| Reject | ||
| </Button> | ||
| <Button | ||
| size="sm" | ||
| onClick={() => | ||
| handleReview(submission.id, "APPROVED") | ||
| } | ||
| > | ||
| Approve | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| })} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
This flow still has no per-stage payout or escrow-release action.
The sponsor queue stops at approve/reject. Unlike the standard submissions flow, there is no mark-paid / transaction-capture step here, so approved milestone work cannot actually be released or audited per stage.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/bounty-detail/bounty-detail-milestone-flow-card.tsx` around lines
289 - 358, The sponsor queue currently only calls handleReview(submission.id,
"APPROVED"/"REJECTED") and lacks a per-stage payout/escrow-release step; add a
release/capture flow that runs when a sponsor approves. Update the UI in
bounty-detail-milestone-flow-card to either replace or supplement the Approve
button with an "Approve & Release" action that calls a new handler (e.g.,
handleSponsorApprove or extend handleReview to accept a "RELEASE" action), and
implement that handler to: 1) call the backend endpoint that performs the escrow
release/transaction capture (suggested function names: releaseMilestonePayout,
captureTransaction, or markMilestonePaid) using submission.id and
submission.milestoneIndex, 2) update local state.milestones and
pendingSubmissions to reflect paid/audited status, and 3) surface errors via the
existing error/logging flow; keep the Reject path unchanged. Ensure you
reference submission.id, submission.milestoneIndex, pendingSubmissions,
state.milestones, and handleReview when making the changes.
| if (bounty.type === "MILESTONE_BASED") { | ||
| return "Join Milestone Flow"; | ||
| } |
There was a problem hiding this comment.
Wire the milestone CTA to the milestone flow, not the GitHub issue.
For MILESTONE_BASED bounties the label changes, but both click handlers still open bounty.githubIssueUrl. That makes the primary action navigate away from the new in-page flow instead of joining or focusing it.
Also applies to: 85-88, 147-149, 159-162
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 38 - 40,
The CTA label change for MILESTONE_BASED bounties is correct, but the click
handlers still open bounty.githubIssueUrl; update the primary and secondary
click handlers in bounty-detail-sidebar-cta.tsx so that when bounty.type ===
"MILESTONE_BASED" they invoke the milestone flow handler instead of navigating
to bounty.githubIssueUrl — i.e., replace calls that open or link to
bounty.githubIssueUrl with the existing milestone flow entry point (e.g.,
openMilestoneFlow / joinMilestoneFlow / onOpenMilestoneModal) used elsewhere in
the app, and apply this change for all instances noted (the other CTA handlers
in the same file). Ensure the conditional uses bounty.type === "MILESTONE_BASED"
to select the milestone handler and leaves non-milestone behavior unchanged.
| function createDefaultMilestones(): MilestoneDefinition[] { | ||
| return [ | ||
| { | ||
| id: "m1", | ||
| title: "Milestone 1 - Build", | ||
| description: "Ship an initial implementation and open a first PR.", | ||
| maxWinners: 8, | ||
| rewardPercentage: 30, | ||
| }, | ||
| { | ||
| id: "m2", | ||
| title: "Milestone 2 - Harden", | ||
| description: | ||
| "Refine implementation quality based on review feedback and testing.", | ||
| maxWinners: 4, | ||
| rewardPercentage: 30, | ||
| }, | ||
| { | ||
| id: "m3", | ||
| title: "Milestone 3 - Finalize", | ||
| description: | ||
| "Deliver the production-ready version and final documentation.", | ||
| maxWinners: 2, | ||
| rewardPercentage: 40, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| function createInitialState(bountyId: string): MilestoneFlowState { | ||
| return { | ||
| bountyId, | ||
| milestones: createDefaultMilestones(), | ||
| participants: [], |
There was a problem hiding this comment.
Parameterize milestone definitions instead of hardcoding them in the hook.
Every milestone bounty gets the same 8 → 4 → 2 stages and 30/30/40 payouts here because useMilestoneFlow only accepts bountyId. The UI and capacity checks will be wrong for any bounty whose actual funnel differs.
Also applies to: 63-67
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/use-milestone-flow.ts` around lines 13 - 45, The hook hardcodes
milestone definitions via createDefaultMilestones and createInitialState so
every bounty gets the same 8→4→2 and 30/30/40 payout regardless of real bounty
config; change the API to accept milestone definitions from the caller: update
useMilestoneFlow to accept an optional initialMilestones parameter (or a full
config object), modify createInitialState to accept that milestones array
(falling back to createDefaultMilestones()), and update createDefaultMilestones
to support parameterized overrides if needed; ensure all places constructing the
state (and tests) pass the real milestone defs so UI and capacity checks use the
bounty-specific funnel.
| const [state, setState] = useLocalStorage<MilestoneFlowState>( | ||
| `${FLOW_KEY_PREFIX}${bountyId}`, | ||
| createInitialState(bountyId), | ||
| ); |
There was a problem hiding this comment.
localStorage cannot back a shared milestone workflow.
This state lives only in the current browser and is user-editable. Contributors, sponsors, and profile pages on other devices or sessions will not see the same joins, submissions, reviews, or occupancy, so the multi-user flow cannot function reliably.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/use-milestone-flow.ts` around lines 64 - 67, The milestone flow state
is stored client-side with useLocalStorage
(`useLocalStorage<MilestoneFlowState>(\`${FLOW_KEY_PREFIX}${bountyId}\`,
createInitialState(bountyId))`), which makes joins/reviews/submissions
local-only and user-editable; change this to a shared, server-backed state:
replace the useLocalStorage usage for MilestoneFlowState with API-backed
persistence (e.g., load/save via your milestones endpoints using bountyId) and
add real-time sync (websocket/subscribe) so updates to the state are written
through the server and broadcast to other clients; update functions that call
setState to call the server save/update methods and reconcile incoming server
events to update the local state.
| const joinFlow = (contributorId: string, contributorName: string) => { | ||
| setState((current) => { | ||
| const existing = current.participants.find( | ||
| (participant) => participant.contributorId === contributorId, | ||
| ); | ||
| if (existing) return current; | ||
|
|
||
| const firstMilestone = current.milestones[0]; | ||
| const occupied = current.participants.filter((participant) => | ||
| isStageOccupied(participant, 0), | ||
| ).length; | ||
|
|
||
| if (occupied >= firstMilestone.maxWinners) { | ||
| throw new Error("Milestone 1 is full. Try again later."); | ||
| } | ||
|
|
||
| const timestamp = nowIso(); | ||
| const newParticipant: MilestoneParticipant = { | ||
| id: `mp_${bountyId}_${Date.now()}`, | ||
| bountyId, | ||
| contributorId, | ||
| contributorName, | ||
| currentMilestoneIndex: 0, | ||
| status: "ACTIVE", | ||
| joinedAt: timestamp, | ||
| updatedAt: timestamp, | ||
| }; | ||
|
|
||
| return { | ||
| ...current, | ||
| participants: [...current.participants, newParticipant], | ||
| updatedAt: timestamp, | ||
| }; | ||
| }); |
There was a problem hiding this comment.
joinFlow only supports a blind join, not a slot application.
The transition records only contributor identity at milestone 0. There is no requested-slot or proposal payload here, so contributors cannot apply for a specific slot and sponsors have nothing to use for slot assignment or reassignment decisions.
| const nextMilestoneIndex = submission.milestoneIndex + 1; | ||
| const nextMilestone = current.milestones[nextMilestoneIndex]; | ||
|
|
||
| const occupiedNextStage = current.participants.filter((item) => | ||
| isStageOccupied(item, nextMilestoneIndex), | ||
| ).length; | ||
|
|
||
| if (occupiedNextStage >= nextMilestone.maxWinners) { | ||
| throw new Error( | ||
| `${nextMilestone.title} has no open slots. Review queue before advancing.`, | ||
| ); | ||
| } | ||
|
|
||
| updatedParticipant = { | ||
| ...participant, | ||
| currentMilestoneIndex: nextMilestoneIndex, | ||
| status: "ACTIVE", | ||
| updatedAt: timestamp, | ||
| }; |
There was a problem hiding this comment.
Advancement is currently "first approved wins", not top-N.
Each approval immediately consumes a slot in the next stage until maxWinners is full. There is no score/ranking or explicit selection step, so later stronger submissions can be blocked purely by review order.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@hooks/use-milestone-flow.ts` around lines 223 - 241, Advancement currently
consumes next-stage slots on approval; change it to perform a top-N selection
instead: when computing advancement in use-milestone-flow (around
submission.milestoneIndex / current.milestones and isStageOccupied usage),
gather all candidates for nextMilestone (existing current.participants whose
currentMilestoneIndex === nextMilestoneIndex plus this submission), sort them by
the canonical score field (e.g., submission.score or participant.score) and
permit advancement only if this submission ranks within
nextMilestone.maxWinners; otherwise mark the submission as
waitlisted/review-pending instead of immediately claiming a slot. Update the
slot-check logic that uses occupiedNextStage and set updatedParticipant.status
and currentMilestoneIndex only for selected winners.
closes #142
Implemented: Model 4 multi-winner milestone flow is now functional on the frontend, including join, submit, sponsor review, funnel advancement, and profile claim visibility.
Summary by CodeRabbit