Skip to content

fix(desktop): stable Adopt button + Adopt-all on v1 workspaces#4214

Merged
Kitenite merged 4 commits intomainfrom
migration-adopt-feedback
May 7, 2026
Merged

fix(desktop): stable Adopt button + Adopt-all on v1 workspaces#4214
Kitenite merged 4 commits intomainfrom
migration-adopt-feedback

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 7, 2026

Summary

  • The v1 → v2 migration modal scrolled to the top of the list when you clicked Adopt, because the row swapped its <Button> for a <div> mid-flight; Radix Dialog then re-focused the dialog root. The adopting state is now a disabled button (with spinner + "Adopting…") so focus stays on the row.
  • Added an Adopt all (N) button in the workspaces page header. It runs sequentially and shows live progress ("Adopting 3/12"); individual rows are disabled while it's running.
  • Lifted per-row adoption state into ImportWorkspacesPage so Adopt-all and per-row clicks share one state machine.

Test plan

  • Open the v1 import modal → Workspaces tab with a long list; click Adopt on a workspace far down → list does not scroll, row shows "Adopting…", then "Imported".
  • Click Adopt all with multiple pending workspaces → sequential adopt, header shows progress, individual Adopt buttons disabled.
  • Trigger an adopt failure (e.g., force NOT_FOUND) → row shows error + Retry; Adopt-all still completes the rest.
  • Already-imported rows still render as Imported and aren't re-adopted by Adopt-all.

Summary by cubic

Stabilizes the Adopt button and adds Adopt‑all with progress in the v1→v2 import modal, with a cleaner Linear‑style UI. Prevents duplicate adopts during bulk runs and ensures adopted workspaces appear in the sidebar.

  • Bug Fixes

    • Keep the adopting state as a disabled button with spinner and “Adopting…”, stopping dialog refocus/scroll.
    • Prevent double‑adopt in Adopt‑all by showing “Queued” instead of Retry on errors and skipping entries already running/imported.
  • New Features

    • “Adopt all (N)” runs sequentially with live progress (“Adopting X/Y”), disables row actions while running, skips already‑imported items, continues on errors with per‑row Retry; adoption state is centralized in ImportWorkspacesPage; sidebar updates and the cloud list is invalidated after adoption.
    • Linear‑style polish: smaller header/footer buttons (h‑8, 13px), 13px row text with flat icons and subtle hover, 10px uppercase section labels, and tabular‑nums in “Adopt all · N”.

Written for commit e89a621. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • "Adopt all" now runs selected imports sequentially with progress tracking and disables per-row actions while running.
  • Improvements

    • Per-workspace import statuses (idle, running, imported, error) surface clearer row actions and enable retry.
    • Row action visuals refined (running state may show a label; clearer imported/error states).
    • Page header accepts an optional header action; modal footer and navigation buttons use consistent sizing and styling.

…kspaces

- Render the adopting state as a disabled button instead of swapping to a
  div so focus stays put and Radix doesn't scroll the dialog to top mid-list.
- Show "Adopting…" with a spinner on the row while the mutation is in flight.
- Add an "Adopt all (N)" button to the workspaces page header that
  sequentially adopts pending workspaces and shows live progress.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd3374b5-510b-4ff7-9ec7-47c23edeed9e

📥 Commits

Reviewing files that changed from the base of the PR and between ecf0b00 and e89a621.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx

📝 Walkthrough

Walkthrough

ImportWorkspacesPage centralizes per-workspace adoption into a page-level AdoptStatus map, precomputes worktreePath/baseBranch per entry, runs sequential "Adopt all" with progress, and passes status/disabled/onAdopt into a presentational WorkspaceRow. ImportPageShell adds headerAction; RowAction supports an optional running label.

Changes

Centralized Workspace Adoption State & UI

Layer / File(s) Summary
Type & State Definitions
.../ImportRow/ImportRow.tsx, .../ImportWorkspacesPage/ImportWorkspacesPage.tsx
RowAction "running" variant gains optional label?: string. AdoptStatus and IDLE added; adoptStates map/ref, updateAdoptStatus, and page-level adoptWorkspace implemented with NOT_FOUND retry, sidebar ensure, and cloud query invalidation.
Visible Workspace Entry Shape
.../ImportWorkspacesPage/ImportWorkspacesPage.tsx
Visible workspace entries include worktreePath and baseBranch derived from selected worktree metadata.
Adopt-All Queue & Processing
.../ImportWorkspacesPage/ImportWorkspacesPage.tsx
"Adopt all" builds a queue from idle/error entries, runs adoptWorkspace sequentially, and tracks adoptAllProgress; headerAction shows spinner/progress when active.
Row Rendering & Wiring
.../ImportWorkspacesPage/ImportWorkspacesPage.tsx
Rows receive entry, status (from adoptStates), disabled during adopt-all, and onAdopt which delegates to page-level adoptWorkspace; WorkspaceRow becomes presentational.
Row UI / RowAction Rendering
.../ImportRow/ImportRow.tsx
Row layout/typography updated; RowActionView running branch renders a disabled outlined Button with Spinner and optional label; error/blocked/confirm/pick styles adjusted.
ImportPageShell Header Action
.../ImportPageShell/ImportPageShell.tsx
ImportPageShellProps adds headerAction?: ReactNode; ImportPageShell renders headerAction in the header next to the conditional refresh button.
Modal Footer Styling
.../V1ImportModal/V1ImportModal.tsx
Footer padding/border and action button variants/sizes standardized; navigation and close handlers unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ImportWorkspacesPage
  participant adoptWorkspace
  participant SidebarState
  participant QueryClient
  User->>ImportWorkspacesPage: clicks "Adopt all" or per-row "Adopt"
  ImportWorkspacesPage->>adoptWorkspace: invoke (entry)
  adoptWorkspace->>SidebarState: ensureWorkspaceInSidebar(v2ProjectId)
  adoptWorkspace->>QueryClient: invalidate cloud list query
  adoptWorkspace-->>ImportWorkspacesPage: success / error
  ImportWorkspacesPage->>User: update row status / adoptAllProgress
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hums, small paws align,

A map of statuses, rows in line.
Each adopt clicks, the spinner sings,
One-by-one the new workspaces spring.
🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title accurately and specifically summarizes the main changes: stabilizing the Adopt button behavior and adding Adopt-all functionality for v1 workspaces.
Description check ✅ Passed The PR description follows the template with a comprehensive summary, related context, type of change (bug fix + new feature), and explicit test plan covering all major scenarios.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch migration-adopt-feedback

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 7, 2026

Greptile Summary

This PR addresses two bugs in the v1 import modal: (1) the workspace list scrolled to the top when clicking Adopt because the row swapped its Button for a div, causing Radix Dialog to re-focus; this is fixed by keeping a disabled Button with a spinner in the \"running\" state. (2) a new sequential "Adopt all" button is added to the page header, with per-workspace adopt state lifted from individual rows into ImportWorkspacesPage.

  • Focus-scroll fix: ImportRow's \"running\" case now renders a disabled Button instead of a div, preserving DOM shape and preventing Radix Dialog focus management from scrolling the list.
  • Adopt-all: adoptAll builds a snapshot queue from a ref, processes workspaces sequentially, tracks progress with adoptAllProgress state, and disables individual "Adopt" (ready-state) buttons while running.
  • State lift: AdoptStatus and adoptWorkspace logic moved from WorkspaceRow into ImportWorkspacesPage; row is now a pure presentation component driven by entry, status, disabled, and onAdopt props.

Confidence Score: 3/5

The core focus-scroll fix and Adopt-all implementation are sound, but the Retry button on error-state rows remains active during an ongoing Adopt-all run, which can trigger concurrent API mutations for the same workspace.

The main logic is well-structured: state is correctly lifted, the ref pattern avoids stale closures in adoptAll, and the sequential loop is straightforward. The gap is that disabled is only threaded into the ready action; error-state rows keep an active Retry button even while adoptAll processes the same workspace, potentially firing two simultaneous adopt.mutate calls.

ImportWorkspacesPage.tsx — the interaction between the adoptAll queue snapshot and error-state Retry buttons deserves a closer look before this ships to users with large v1 workspace lists.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx Core change: lifts per-row adopt state into parent, adds sequential Adopt-all with live progress. Has a concurrency gap where error-row Retry buttons remain active during adoptAll, potentially triggering double API calls for the same workspace.
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/components/ImportPageShell/ImportPageShell.tsx Minor layout change: wraps the existing refresh button in a flex container and adds an optional headerAction slot before it. Straightforward and safe.
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/components/ImportRow/ImportRow.tsx The "running" case now renders a disabled Button (with optional label) instead of a plain div, fixing the Radix Dialog focus-scroll issue. Clean and correct.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx:412-413
**Retry button active during Adopt-all**

When `isAdoptingAll` is true, the `disabled` flag is only threaded into the `"ready"` action branch; the `"error"` branch emits `{ kind: "error", onRetry: onAdopt }` without it. The `RowActionView` error case renders an always-enabled Retry button, so a user can click Retry on an error-state row while an `adoptAll` run is already processing that same workspace (it was included in the queue snapshot). This results in two concurrent `adoptWorkspace` calls for the same workspace — two parallel `adopt.mutate` API requests — which may create duplicate entries or produce a misleading double-error.

### Issue 2 of 3
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx:302-307
**`adoptAll` does not skip workspaces already transitioned to `"running"` mid-queue**

`adoptAll` builds the queue once from the ref snapshot and then iterates without re-checking state. If a workspace was `"error"` when the queue was built (and thus included), but the user clicked Retry on it before `adoptAll` reaches it, `adoptWorkspace` will be called a second time even though the workspace is already mid-flight. A guard like `if (adoptStatesRef.current.get(entry.workspace.id)?.kind === "running") continue;` at the top of each loop iteration would prevent the duplicate call.

### Issue 3 of 3
apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx:328-330
**Redundant `isAdoptingAll &&` guard on `adoptAllProgress`**

`isAdoptingAll` is defined as `adoptAllProgress !== null`, so `isAdoptingAll && adoptAllProgress` is always the same as just `adoptAllProgress`. The extra condition implies `adoptAllProgress` could be null while `isAdoptingAll` is true, which is impossible. Replacing the ternary condition with just `adoptAllProgress` makes the intent clearer.

Reviews (1): Last reviewed commit: "fix(desktop): keep adopting button stabl..." | Re-trigger Greptile

Comment on lines +412 to +413
if (status.kind === "error") {
return { kind: "error", message: status.message, onRetry: onAdopt };
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 Retry button active during Adopt-all

When isAdoptingAll is true, the disabled flag is only threaded into the "ready" action branch; the "error" branch emits { kind: "error", onRetry: onAdopt } without it. The RowActionView error case renders an always-enabled Retry button, so a user can click Retry on an error-state row while an adoptAll run is already processing that same workspace (it was included in the queue snapshot). This results in two concurrent adoptWorkspace calls for the same workspace — two parallel adopt.mutate API requests — which may create duplicate entries or produce a misleading double-error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx
Line: 412-413

Comment:
**Retry button active during Adopt-all**

When `isAdoptingAll` is true, the `disabled` flag is only threaded into the `"ready"` action branch; the `"error"` branch emits `{ kind: "error", onRetry: onAdopt }` without it. The `RowActionView` error case renders an always-enabled Retry button, so a user can click Retry on an error-state row while an `adoptAll` run is already processing that same workspace (it was included in the queue snapshot). This results in two concurrent `adoptWorkspace` calls for the same workspace — two parallel `adopt.mutate` API requests — which may create duplicate entries or produce a misleading double-error.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +302 to +307
for (let i = 0; i < queue.length; i++) {
const entry = queue[i];
if (!entry) continue;
setAdoptAllProgress({ current: i, total: queue.length });
await adoptWorkspace(entry);
}
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 adoptAll does not skip workspaces already transitioned to "running" mid-queue

adoptAll builds the queue once from the ref snapshot and then iterates without re-checking state. If a workspace was "error" when the queue was built (and thus included), but the user clicked Retry on it before adoptAll reaches it, adoptWorkspace will be called a second time even though the workspace is already mid-flight. A guard like if (adoptStatesRef.current.get(entry.workspace.id)?.kind === "running") continue; at the top of each loop iteration would prevent the duplicate call.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx
Line: 302-307

Comment:
**`adoptAll` does not skip workspaces already transitioned to `"running"` mid-queue**

`adoptAll` builds the queue once from the ref snapshot and then iterates without re-checking state. If a workspace was `"error"` when the queue was built (and thus included), but the user clicked Retry on it before `adoptAll` reaches it, `adoptWorkspace` will be called a second time even though the workspace is already mid-flight. A guard like `if (adoptStatesRef.current.get(entry.workspace.id)?.kind === "running") continue;` at the top of each loop iteration would prevent the duplicate call.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +328 to +330
{isAdoptingAll && adoptAllProgress
? `Adopting ${adoptAllProgress.current + 1}/${adoptAllProgress.total}`
: `Adopt all (${pendingEntries.length})`}
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 Redundant isAdoptingAll && guard on adoptAllProgress

isAdoptingAll is defined as adoptAllProgress !== null, so isAdoptingAll && adoptAllProgress is always the same as just adoptAllProgress. The extra condition implies adoptAllProgress could be null while isAdoptingAll is true, which is impossible. Replacing the ternary condition with just adoptAllProgress makes the intent clearer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx
Line: 328-330

Comment:
**Redundant `isAdoptingAll &&` guard on `adoptAllProgress`**

`isAdoptingAll` is defined as `adoptAllProgress !== null`, so `isAdoptingAll && adoptAllProgress` is always the same as just `adoptAllProgress`. The extra condition implies `adoptAllProgress` could be null while `isAdoptingAll` is true, which is impossible. Replacing the ternary condition with just `adoptAllProgress` makes the intent clearer.

How can I resolve this? If you propose a fix, please make it concise.

- 13px row text, flat icons (no chip background), subtle row hover bg.
- Smaller header (14px medium tracking-tight, 12px description), border/60.
- Section labels become 10px semibold uppercase tracking-wider muted/70.
- Buttons standardized to h-7 with 12px font; modal footer to h-8 13px.
- Adopt-all label uses tabular-nums and "·" separator.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-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.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx:306">
P2: Add a guard at the top of the loop iteration to skip workspaces that are already in `"running"` state. Since the queue is built once from a snapshot but execution is sequential and async, a workspace's state could change mid-queue (e.g., via a concurrent Retry click). Without re-checking, `adoptWorkspace` may be called redundantly for an already-in-flight workspace.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx:412">
P2: The Retry button in the error state is not disabled during an adopt-all run. The `disabled` prop is only threaded into the `"ready"` action branch but not the `"error"` branch. This allows a user to click Retry on an errored row while adopt-all is actively processing the queue (which includes that same errored workspace), resulting in two concurrent `adoptWorkspace` calls for the same workspace — potentially creating duplicates or confusing error states.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Copy link
Copy Markdown
Contributor

@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
`@apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx`:
- Around line 375-385: The Retry button can be clicked during an Adopt-all run
causing duplicate adopt calls; fix by honoring the disabled flag in the error
branch or skipping running entries: update the error-branch RowAction generation
(the code path where status.kind === "error" in ImportRow/WorkspaceRow) to
respect the passed disabled prop (isAdoptingAll) so Retry is rendered disabled
when isAdoptingAll is true, and/or modify adoptWorkspace or the adopt-all loop
(the function that iterates over queue and calls adoptWorkspace) to skip entries
that already have an in-progress state in adoptStates (or a running flag) to
prevent calling client.workspaceCreation.adopt.mutate twice for the same entry.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 588c1d94-94af-4f88-a86b-01c886f9d26a

📥 Commits

Reviewing files that changed from the base of the PR and between 56a1f50 and 24ddbdd.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/ImportWorkspacesPage/ImportWorkspacesPage.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/components/ImportPageShell/ImportPageShell.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/V1ImportModal/components/ImportRow/ImportRow.tsx

Kitenite added 2 commits May 7, 2026 15:35
- adoptWorkspace no longer returns a never-read boolean.
- AdoptStatus.imported drops the unread v2WorkspaceId field.
- Skip the redundant initial setAdoptAllProgress; the loop's first
  iteration writes it.
Adopt-all snapshots its queue, so a manual Retry click on an errored
row could fire adopt.mutate twice for the same workspace. Two fixes:

- During adopt-all, the row's "error" action becomes a disabled
  "Queued" button instead of an active Retry.
- The adopt-all loop re-checks each entry's live status and skips
  entries already running or imported.

Also drop a redundant isAdoptingAll guard (adoptAllProgress alone is
the truth).
@Kitenite Kitenite merged commit f4ae66e into main May 7, 2026
9 of 10 checks passed
@Kitenite Kitenite deleted the migration-adopt-feedback branch May 7, 2026 22:40
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 8, 2026
Recorded as integrated via -s ours after batch PRs #455-#464.

Taken via individual PRs:
- PR  1 (#455): v2 polish 前半 safe set (9 commits)
- PR  2 (#456): v2/host-service polish 中盤 (12 commits)
- PR  3 (#457): sidebar polish + jwt API (5 commits)
- PR  4 (#458): host-service tRPC retry/cache/timeout (3 commits)
- PR  5 (#459): v2 diff pane / file pane polish (2 commits)
- PR  7 (#462): host-service v2 canonical workspace.create + attachment store (PR1 superset-sh#3893 + PR2 superset-sh#3916)
- PR 11 (#463): agents API + onboarding (7 commits + 1 cleanup)
- PR 12 (#464): v1→v2 import flow rewrite (11 commits + 2 follow-ups)
- PR 13 (#460): host-service shell env probe + typo (2 commits)
- PR 16 (#461): marketplace 19 themes (1 commit)

Skipped / deferred (recorded as integrated for behind=0):
- PR  6: CLI v1 launch (superset-sh#3898 + 30+ CLI/SDK followups) — defer to dedicated migration
- PR  9: v2 PR3 (superset-sh#3940) + revert (superset-sh#4017) — net-zero pair
- PR 10: pty-daemon (superset-sh#3896, superset-sh#3971, superset-sh#4054) — fork keeps its terminal-host
- PR 14: Slack MCP-v2 (superset-sh#4197, superset-sh#4208) — depends on mcp-v2/sdk divergence
- PR 15: onboarding remaining (superset-sh#4115, superset-sh#4125, superset-sh#4214, superset-sh#4213, superset-sh#4222, superset-sh#4225) — depends on fork's deleted setup pages

Behind: 0 after this merge.
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.

1 participant