Skip to content

feat(desktop): remove skip-all onboarding affordance#4113

Merged
AviPeltz merged 18 commits into
mainfrom
v2-onboarding-flow-review
May 6, 2026
Merged

feat(desktop): remove skip-all onboarding affordance#4113
AviPeltz merged 18 commits into
mainfrom
v2-onboarding-flow-review

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented May 5, 2026

Summary

  • Remove the chrome-level "Skip onboarding" button from OnboardingProgress and drop the skipAll action (plus its skipped_all analytics event) from the onboarding store
  • Per-step "Skip for now" links via markSkipped() are unchanged — users can still skip individual steps

Test plan

  • Launch desktop in v2 onboarding, confirm no "Skip onboarding" button appears in the top chrome
  • Each step's "Skip for now" link still works and advances the flow
  • Required steps (providers, project) still gate /welcome until completed or skipped
  • bun run lint and bun run typecheck pass

Summary by cubic

Removed the global “Skip onboarding,” stacked Claude Code and Codex with per‑provider Connect and inline Reconfigure/Cancel, and standardized bottom Continue/“Skip for now” across steps (incl. gh-cli and adopt-worktrees). Added OpenAI OAuth localhost loopback that auto‑completes via renderer polling and refreshed connect headers with the Superset brand glyph in 44×44 tiles matching squared provider logos.

  • New Features

    • Auto-capture OpenAI OAuth callback via localhost; the renderer polls consumeOpenAIOAuthCallback to complete auth and routes back to /setup/providers. Loopback stops on cancel/complete and falls back if the port can’t bind.
    • Select worktrees to import in adopt-worktrees via checkboxes (with Select all); new workspaces.importExternalWorktrees accepts paths, and selectExternalWorktreesForImport is used by both the query and import to keep the UI in sync (with tests).
    • Use the Superset brand glyph in 44×44 rounded tiles and align Codex/Claude tile shapes/sizes for visual consistency.
  • Bug Fixes

    • Loopback binds to the redirect_uri host (supports localhost, 127.0.0.1, and ::1) to prevent bind failures on IPv6 systems.
    • Fixed reconnect flows getting stuck on OAuth subroutes by driving navigation from onAuthStateChange back to /setup/providers.

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

Summary by CodeRabbit

  • Chores

    • Removed the global "Skip onboarding" action—onboarding must be completed step-by-step; Back navigation still works.
  • New Features

    • Per-step "Skip for now" options added on several setup screens (worktrees, GH CLI, etc.).
    • Provider setup redesigned to show provider sections concurrently with clearer connect/configure actions.
    • Added an OpenAI OAuth loopback flow to simplify OAuth sign-in via a local callback.
    • Hook callback support to notify UI after OpenAI auth state changes.
    • New API endpoint to consume OpenAI OAuth callbacks.
  • Refactor

    • Streamlined onboarding controls and layout for more consistent step rendering.

Per-step skipping is enough — drop the chrome-level "Skip onboarding"
button and the associated `skipAll` store action. Each step's
"Skip for now" link is unchanged.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 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
📝 Walkthrough

Walkthrough

Removes the global onboarding "skip all" capability, adds per-step "Skip for now" actions in several setup pages, refactors providers setup from a Tabs flow to independent ProviderSection components with per-provider state, and adds an OpenAI OAuth loopback utility plus chat-service and router integration to capture OAuth redirects locally.

Changes

Onboarding Skip Removal & Per-step Skip UI

Layer / File(s) Summary
Store Interface & Implementation
apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts
Removed skipAll() from OnboardingActions and deleted its implementation from the onboarding store.
Progress UI
apps/desktop/src/renderer/routes/_authenticated/setup/components/OnboardingProgress/OnboardingProgress.tsx
Removed local skipAll state and handleSkipAll; deleted "Skip onboarding" button and replaced right-side control with an empty spacer; adjusted backTo / currentIdx initialization order.
Adopt Worktrees UI
apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx
NothingToAdopt branch now renders two actions: Continue and a new Skip for now action.
GH CLI UI
apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx
Added a link-styled Skip for now SetupButton wired to existing handleSkip, placed after the Continue button.

Providers Setup Refactor

Layer / File(s) Summary
Imports & Types
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Removed Tabs import and deleted the public Provider type alias; adjusted React typings.
State Shape
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Replaced single provider state with per-provider connection/method state: claudeMethod, codexMethod, and per-provider reconfiguringClaude/reconfiguringCodex flags.
Behavior / Navigation
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Added handleConnect(base, method) helper to navigate to provider-specific config routes; subtitle adapts when at least one provider is connected.
UI Composition
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Replaced Tabs UI with two ProviderSection blocks (Claude Code, Codex) using ConnectedPanel/ProviderOptionCard composition; per-provider connect/reconfigure flows implemented.
Local Components & Types
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Added ProviderSectionProps and implemented ProviderSection component handling connected, option, and reconfigure flows.
Actions / Bottom Bar
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
Bottom bar shows Continue when at least one provider is connected and a Skip for now action otherwise; onboarding navigation and mark-complete/skip wiring preserved.

OpenAI OAuth Loopback & Chat Service Integration

Layer / File(s) Summary
Parsing & Loopback Types
packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts
Added LoopbackTarget interface and `parseLoopbackTargetFromAuthUrl(authUrl: string): LoopbackTarget
Loopback Server Implementation
packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts
Added OpenAIOAuthLoopback class with start(options) and stop() implementing a local HTTP server that captures OAuth redirects, serves success/error pages, and invokes callbacks for onCallback/onError.
Chat Service Wiring
packages/chat/src/server/desktop/chat-service/chat-service.ts
Imported loopback utilities; added openAIOAuthLoopback and pendingOpenAIOAuthCallbackUrl fields; updated startOpenAIOAuth, cancelOpenAIOAuth, and completeOpenAIOAuth to manage loopback lifecycle and stash/clear pending callback URLs.
Router Endpoint
packages/chat/src/server/desktop/router/router.ts
Added consumeOpenAIOAuthCallback endpoint that delegates to service.consumeOpenAIOAuthCallback().

useOpenAIOAuth Hook: UI callback & completion changes

Layer / File(s) Summary
API Shape
apps/desktop/src/renderer/components/Chat/.../useOpenAIOAuth.ts
Added optional `onAuthStateChange?: () => Promise
Sync & Completion Behavior
apps/desktop/src/renderer/components/Chat/.../useOpenAIOAuth.ts
syncOpenAIAuthUi now awaits optional onAuthStateChange after refetch; completeOpenAIOAuth accepts optional codeOverride?: string and uses trimmed code from override or current oauthCode, with UI reset and model selector opening preserved.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as User Browser
  participant ChatService as ChatService (desktop)
  participant Loopback as OpenAIOAuthLoopback (local server)
  participant OpenAI as OpenAI Auth

  ChatService->>OpenAI: initiate OAuth (returns auth URL with redirect_uri)
  Browser->>OpenAI: user authenticates
  OpenAI-->>Browser: redirect to loopback redirect_uri (http://localhost:PORT/path?code=...)
  Browser->>Loopback: GET /path?code=...
  Loopback->>ChatService: onCallback(callbackUrl)
  ChatService->>OpenAI: exchange code for token (complete OAuth)
  ChatService->>Loopback: stop()
  Loopback-->>Browser: render success page
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I sniffed the paths and hopped around,
Took out the blanket skip I found.
Small doors to skip sit by each step,
Two providers now share one rep.
A loopback server caught the key — hop, OAuth happily!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive The pull request description covers the main objective (removing skip-all button) but is primarily auto-generated and lacks detail on several implemented features like OAuth loopback, provider refactoring, and worktree selection. The author-provided description should explicitly document the OAuth loopback implementation, provider page refactoring, and per-step skip additions as these represent significant changes not clearly captured in the manual summary section.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: removing the skip-all onboarding affordance, which is the central theme of this PR.
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 v2-onboarding-flow-review

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
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR removes the chrome-level "Skip onboarding" button from OnboardingProgress and drops the corresponding skipAll store action (including its skipped_all analytics event). Per-step "Skip for now" links via markSkipped are untouched, and the onboarding_finished event continues to be emitted from adopt-worktrees/page.tsx when the last step is finished or skipped.

  • OnboardingProgress.tsx: Removes the skipAll subscription, handleSkipAll handler, and the button element; the third grid column now renders as an empty <div> to preserve centering of the step pills.
  • onboardingStore.ts: Deletes skipAll from the OnboardingActions interface and its implementation, which had marked all incomplete steps as skipped and emitted onboarding_finished with outcome: \"skipped_all\".

Confidence Score: 5/5

Safe to merge — the change is a clean removal of a single UI affordance and its backing store action, with no side effects on the remaining onboarding paths.

Both files contain straightforward deletions. The onboarding_finished analytics event is still emitted via adopt-worktrees/page.tsx for all remaining user paths, and markSkipped / markComplete are untouched. The only leftover is an empty grid-spacer <div> in the component, which is cosmetic.

No files require special attention. The empty <div> on line 103 of OnboardingProgress.tsx is worth a quick look but has no functional impact.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/setup/components/OnboardingProgress/OnboardingProgress.tsx Removes the skipAll store subscription, handleSkipAll handler, and "Skip onboarding" button; leaves an empty div as a grid spacer for the third column.
apps/desktop/src/renderer/stores/onboarding/onboardingStore.ts Drops the skipAll action and its skipped_all analytics event from the store interface and implementation; markSkipped/markComplete paths are unchanged.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User enters onboarding] --> B[providers step]
    B -->|markComplete / markSkipped| C[gh-cli step]
    C -->|markComplete / markSkipped| D[permissions step]
    D -->|markComplete / markSkipped| E[project step]
    E -->|markComplete / markSkipped| F[adopt-worktrees step]
    F -->|finishFlow| G["track onboarding_finished\n(outcome: completed)"]
    F -->|skipFlow| H["track onboarding_finished\n(outcome: skipped)"]
    G --> I[Navigate to /welcome or project]
    H --> I
    style J fill:#f88,stroke:#c00
    J["REMOVED: Skip onboarding button\n(skipAll → skipped_all event)"]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/routes/_authenticated/setup/components/OnboardingProgress/OnboardingProgress.tsx:103
**Empty layout spacer div**

The right-hand column is now empty after the button removal. Its only job is to balance the `grid-cols-[1fr_auto_1fr]` grid so the step pills remain visually centered. A brief comment would make this intentional spacer obvious to future readers; alternatively, the Tailwind classes on the div (`flex items-center justify-end`) can be dropped since there is no longer any content to align.

Reviews (1): Last reviewed commit: "feat(desktop): remove skip-all onboardin..." | Re-trigger Greptile

Skip onboarding
</button>
</div>
<div className="flex items-center justify-end" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Empty layout spacer div

The right-hand column is now empty after the button removal. Its only job is to balance the grid-cols-[1fr_auto_1fr] grid so the step pills remain visually centered. A brief comment would make this intentional spacer obvious to future readers; alternatively, the Tailwind classes on the div (flex items-center justify-end) can be dropped since there is no longer any content to align.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/setup/components/OnboardingProgress/OnboardingProgress.tsx
Line: 103

Comment:
**Empty layout spacer div**

The right-hand column is now empty after the button removal. Its only job is to balance the `grid-cols-[1fr_auto_1fr]` grid so the step pills remain visually centered. A brief comment would make this intentional spacer obvious to future readers; alternatively, the Tailwind classes on the div (`flex items-center justify-end`) can be dropped since there is no longer any content to align.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- Show Claude Code and Codex on the same providers page (no more
  tab toggle); each has its own selectable connection methods and
  Connect button, stacked vertically.
- Ensure every step renders both Continue (primary) and "Skip for
  now" (link) in a stable bottom column. Adds the missing skip on
  the gh-cli installed branch and the adopt-worktrees empty branch.
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/setup/providers/page.tsx`:
- Around line 215-234: ProviderSectionProps declares onReconfigure but
ProviderSection doesn't use it; update ProviderSection to accept and forward
that callback: add onReconfigure to the destructured params in the
ProviderSection function and inject it into the connectedPanel by checking
React.isValidElement(connectedPanel) and returning
React.cloneElement(connectedPanel, { onReconfigure }) so the ConnectedPanel
instance receives the handler; ensure React is imported if not already so
cloneElement/isValidElement work. This consolidates the reconfigure entry point
at ProviderSection without changing the call sites that already pass
onReconfigure.
🪄 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: d92e04f1-3ba1-429f-b218-97aa82c415e6

📥 Commits

Reviewing files that changed from the base of the PR and between 6c251f7 and 7b235ee.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/setup/adopt-worktrees/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/setup/gh-cli/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx

- Add a localhost loopback HTTP server that binds to the redirect_uri
  port (1455) advertised in the OpenAI authorize URL. When the browser
  hits /auth/callback, the server passes the URL straight to
  completeOpenAIOAuth so the user no longer has to copy/paste it. Falls
  back silently to the existing manual paste flow if the port can't
  be bound.
- Tidy the providers page reconfigure UX: move Reconfigure/Cancel to a
  small inline link in the section header so the in-flow stack is
  just option cards + a single primary button instead of three stacked
  buttons.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

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: 2

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx (1)

39-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

useRef prevents shouldAutoAdvance from ever becoming true — auto-advance silently broken.

wasConfiguredOnMount is a useRef, so mutating .current inside the effect (line 42) does not schedule a re-render. The execution order is:

  1. Queries resolve → React re-renders; at render time wasConfiguredOnMount.current is still nullshouldAutoAdvance = false.
  2. The capturing effect runs and sets .current = atLeastOneConnected (potentially true).
  3. Nothing triggers another render — this component only subscribes to completed.providers, manualWalkthrough, markComplete, markSkipped, and goTo (all stable function refs). goTo("providers") writes currentStep to the store, which no selector here watches.
  4. The auto-advance effect sees shouldAutoAdvance = false and never navigates.

The result is that returning users who already have a provider connected get stuck on this page until they click Continue manually, even though the intent is to auto-skip them.

Use useState so the "captured on mount" value triggers the required re-render:

🐛 Proposed fix: replace `useRef` with `useState`
-import { type ReactNode, useEffect, useRef, useState } from "react";
+import { type ReactNode, useEffect, useState } from "react";
-	const wasConfiguredOnMount = useRef<boolean | null>(null);
-	useEffect(() => {
-		if (!isStatusPending && wasConfiguredOnMount.current === null) {
-			wasConfiguredOnMount.current = atLeastOneConnected;
-		}
-	}, [isStatusPending, atLeastOneConnected]);
+	const [wasConfiguredOnMount, setWasConfiguredOnMount] = useState<boolean | null>(null);
+	useEffect(() => {
+		if (!isStatusPending && wasConfiguredOnMount === null) {
+			setWasConfiguredOnMount(atLeastOneConnected);
+		}
+	}, [isStatusPending, atLeastOneConnected, wasConfiguredOnMount]);
-	const shouldAutoAdvance =
-		!completed && !manualWalkthrough && wasConfiguredOnMount.current === true;
+	const shouldAutoAdvance =
+		!completed && !manualWalkthrough && wasConfiguredOnMount === true;

setWasConfiguredOnMount fires exactly once (guarded by === null), schedules a re-render, and shouldAutoAdvance evaluates correctly in the subsequent render.

Also applies to: 55-56

🤖 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 `@apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx`
around lines 39 - 44, The issue is that wasConfiguredOnMount is a useRef so
setting wasConfiguredOnMount.current inside the effect doesn't trigger a
re-render and prevents shouldAutoAdvance from ever becoming true; replace the
useRef usage with useState (e.g., const [wasConfiguredOnMount,
setWasConfiguredOnMount] = useState<boolean|null>(null)), update the effect to
call setWasConfiguredOnMount(atLeastOneConnected) guarded by
wasConfiguredOnMount === null so it only fires once, and update any subsequent
reads of wasConfiguredOnMount.current to use the state variable so
shouldAutoAdvance recomputes and the auto-advance effect can navigate as
intended (also apply the same change for the second occurrence referenced in the
review).
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx (1)

27-35: ⚡ Quick win

Auth query error states are silently swallowed.

Neither isError nor error is destructured from the two useQuery calls. When either query fails (network error, service down, etc.), anthropicAuthStatus / openAIAuthStatus are undefined, both providers appear as "not connected", and the user gets no indication something went wrong.

Consider surfacing a brief inline error with select-text cursor-text classes per the project's coding guidelines for renderer error text.

♻️ Suggested approach
-	const { data: anthropicAuthStatus, isPending: isAnthropicPending } =
-		chatServiceTrpc.auth.getAnthropicStatus.useQuery();
-	const { data: openAIAuthStatus, isPending: isOpenAIPending } =
-		chatServiceTrpc.auth.getOpenAIStatus.useQuery();
+	const {
+		data: anthropicAuthStatus,
+		isPending: isAnthropicPending,
+		isError: isAnthropicError,
+	} = chatServiceTrpc.auth.getAnthropicStatus.useQuery();
+	const {
+		data: openAIAuthStatus,
+		isPending: isOpenAIPending,
+		isError: isOpenAIError,
+	} = chatServiceTrpc.auth.getOpenAIStatus.useQuery();

Then in the render, when isAnthropicError || isOpenAIError, show a small error notice:

{(isAnthropicError || isOpenAIError) && (
  <p className="select-text cursor-text text-[11px] text-red-400">
    Failed to load provider status. Please restart the app and try again.
  </p>
)}

As per coding guidelines, error text must carry select-text cursor-text because the renderer sets user-select: none on body.

🤖 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 `@apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx`
around lines 27 - 35, The auth queries
(chatServiceTrpc.auth.getAnthropicStatus.useQuery and
chatServiceTrpc.auth.getOpenAIStatus.useQuery) currently only destructure
data/isPending, so network/query errors are swallowed; update both calls to also
destructure isError and/or error (e.g., isAnthropicError, anthropicError,
isOpenAIError, openAIError) and use those flags in the render to show a small
inline error when either is true, e.g., render a <p> with classes "select-text
cursor-text text-[11px] text-red-400" that says the status failed to load and
suggests restarting the app; ensure existing connected checks (claudeConnected,
codexConnected) still use anthropicAuthStatus/openAIAuthStatus but do not hide
the error message when the queries errored.
🤖 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 `@packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts`:
- Around line 39-93: start() can leave a listening socket open if stop() is
called before listen() completes; add a pendingServer and stopRequested flag to
track the server created in start() (before calling listen) and to allow stop()
to close it. Specifically: in start(), assign the created server to a local
pendingServer and register the error listener; when calling listen(), in the
listen callback check stopRequested — if true, close pendingServer and don't set
this.server; otherwise set this.server and clear pendingServer; in stop(), set
stopRequested = true, close and clear pendingServer if present, and if
this.server exists close and null it; ensure you still remove the onListenError
listener on successful listen or when closing pendingServer so no leaks occur.
- Around line 15-30: The parser currently discards the redirect hostname; update
LoopbackTarget and LoopbackOptions to include a host:string and modify
parseLoopbackTargetFromAuthUrl to return the validated redirectUri.hostname
(preserving "localhost", "127.0.0.1" or "[::1]") alongside port and path; then
thread that host through to wherever start() constructs the listen options and
call server.listen(options.port, options.host) instead of hardcoding "127.0.0.1"
so the server binds to the same loopback address the OAuth redirect expects.

---

Outside diff comments:
In `@apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx`:
- Around line 39-44: The issue is that wasConfiguredOnMount is a useRef so
setting wasConfiguredOnMount.current inside the effect doesn't trigger a
re-render and prevents shouldAutoAdvance from ever becoming true; replace the
useRef usage with useState (e.g., const [wasConfiguredOnMount,
setWasConfiguredOnMount] = useState<boolean|null>(null)), update the effect to
call setWasConfiguredOnMount(atLeastOneConnected) guarded by
wasConfiguredOnMount === null so it only fires once, and update any subsequent
reads of wasConfiguredOnMount.current to use the state variable so
shouldAutoAdvance recomputes and the auto-advance effect can navigate as
intended (also apply the same change for the second occurrence referenced in the
review).

---

Nitpick comments:
In `@apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx`:
- Around line 27-35: The auth queries
(chatServiceTrpc.auth.getAnthropicStatus.useQuery and
chatServiceTrpc.auth.getOpenAIStatus.useQuery) currently only destructure
data/isPending, so network/query errors are swallowed; update both calls to also
destructure isError and/or error (e.g., isAnthropicError, anthropicError,
isOpenAIError, openAIError) and use those flags in the render to show a small
inline error when either is true, e.g., render a <p> with classes "select-text
cursor-text text-[11px] text-red-400" that says the status failed to load and
suggests restarting the app; ensure existing connected checks (claudeConnected,
codexConnected) still use anthropicAuthStatus/openAIAuthStatus but do not hide
the error message when the queries errored.
🪄 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: 51535122-11bc-45fe-a296-b610f9a9235f

📥 Commits

Reviewing files that changed from the base of the PR and between 7b235ee and ffe52dd.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/routes/_authenticated/setup/providers/page.tsx
  • packages/chat/src/server/desktop/chat-service/chat-service.ts
  • packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts

Comment thread packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts Outdated
Comment on lines +39 to +93
async start(options: LoopbackOptions): Promise<void> {
return new Promise<void>((resolve, reject) => {
const server = createServer((req, res) => {
try {
const requestUrl = new URL(
req.url ?? "/",
`http://127.0.0.1:${options.port}`,
);
if (requestUrl.pathname !== options.path) {
res.writeHead(404, { "content-type": "text/plain" });
res.end("Not found");
return;
}

const error = requestUrl.searchParams.get("error");
const code = requestUrl.searchParams.get("code");
if (error || !code) {
const message = error ?? "Missing authorization code";
res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
res.end(renderErrorPage(message));
options.onError?.(new Error(message));
return;
}

res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(SUCCESS_PAGE);
options.onCallback(requestUrl.toString());
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
try {
res.writeHead(500, { "content-type": "text/plain" });
res.end(message);
} catch {
// Response may have already been sent — ignore.
}
options.onError?.(err instanceof Error ? err : new Error(message));
}
});

const onListenError = (err: Error) => reject(err);
server.once("error", onListenError);
server.listen(options.port, "127.0.0.1", () => {
server.off("error", onListenError);
this.server = server;
resolve();
});
});
}

stop(): void {
if (this.server) {
this.server.close();
this.server = null;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts

Repository: superset-sh/superset

Length of output: 5022


🏁 Script executed:

# Also check if there are any tests or usage patterns
rg -A 5 -B 5 "OpenAIOAuthLoopback" packages/chat --type ts --type tsx

Repository: superset-sh/superset

Length of output: 92


🏁 Script executed:

# Check the full class definition to understand initialization
ast-grep --pattern $'export class OpenAIOAuthLoopback {
  $$$
}'

Repository: superset-sh/superset

Length of output: 6150


Cancel pending start() when stop() is called.

this.server is only populated after listen() succeeds. If stop() is called before the listen callback fires, the pending server is never stored or closed, leaving a stale listening socket that can receive requests and invoke callbacks after the promise resolves.

Add tracking for the pending server so that calling stop() before listen completes will close or discard the listener before it becomes active. The suggested approach using pendingServer and a stopRequested flag correctly handles this:

  • Store the server in pendingServer before calling listen()
  • Check the stopRequested flag in the listen callback
  • If stop was requested, close the server without storing it in this.server
  • This ensures cleanup regardless of timing
🤖 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 `@packages/chat/src/server/desktop/chat-service/openai-oauth-loopback.ts`
around lines 39 - 93, start() can leave a listening socket open if stop() is
called before listen() completes; add a pendingServer and stopRequested flag to
track the server created in start() (before calling listen) and to allow stop()
to close it. Specifically: in start(), assign the created server to a local
pendingServer and register the error listener; when calling listen(), in the
listen callback check stopRequested — if true, close pendingServer and don't set
this.server; otherwise set this.server and clear pendingServer; in stop(), set
stopRequested = true, close and clear pendingServer if present, and if
this.server exists close and null it; ensure you still remove the onListenError
listener on successful listen or when closing pendingServer so no leaks occur.

Reconfigure flows previously got stuck on the OAuth subroute because
the navigate-back effect required wasAuthedOnMount to be false — true
for any reconnect.

- Drive both Claude and Codex back to /setup/providers from the OAuth
  hook's onAuthStateChange callback (fires whether the credential was
  fresh or refreshed). Drop the wasAuthedOnMount gating.
- Wire the OpenAI loopback through the renderer: the server stashes
  the callback URL, the renderer polls a new consumeOpenAIOAuthCallback
  query while the dialog is open and runs the existing
  completeOpenAIOAuth mutation, so the dialog dismissal +
  navigate-back path is the same for manual paste and auto-capture.
- Add onAuthStateChange parameter to useOpenAIOAuth (already existed
  on useAnthropicOAuth).
… IPv4

parseLoopbackTargetFromAuthUrl now preserves the host from the redirect
URI (stripping IPv6 brackets) and threads it through to server.listen,
so an OpenAI client configured with [::1] or a system that resolves
localhost to IPv6 won't fail to bind.
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: 2

🤖 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 `@packages/chat/src/server/desktop/chat-service/chat-service.ts`:
- Around line 411-414: consumeOpenAIOAuthCallback currently clears
pendingOpenAIOAuthCallbackUrl immediately, which loses the callback if
completeOpenAIOAuth fails; change the behavior so the
pendingOpenAIOAuthCallbackUrl is not cleared by consumeOpenAIOAuthCallback
itself and instead is cleared only on successful completion (i.e., clear inside
completeOpenAIOAuth on success) or add an explicit ack method (e.g.,
ackOpenAIOAuthCallback) that the renderer calls after success; update references
to pendingOpenAIOAuthCallbackUrl, consumeOpenAIOAuthCallback(),
completeOpenAIOAuth(), and any polling code to use the new non-destructive
consume + explicit clear or clear-on-success flow.
- Around line 389-405: The current bare catch around loopback.start() swallows
all errors; change it to only silence expected bind failures (e.g., EADDRINUSE,
EACCES) and rethrow or log unexpected errors. Specifically, wrap the await
loopback.start(...) in try/catch, inspect the caught error.code (or instance)
inside the catch: if it's an address/bind error (EADDRINUSE, EACCES, etc.) call
loopback.stop() and proceed with the manual-paste fallback, otherwise log the
error via this.logger.error (or rethrow) so unexpected failures in
loopback.start() are surfaced; keep the assignment this.openAIOAuthLoopback =
loopback only on success.
🪄 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: 2e4f1b3e-483f-442f-8fcd-abbd121b9cd8

📥 Commits

Reviewing files that changed from the base of the PR and between ffe52dd and c6770aa.

📒 Files selected for processing (5)
  • apps/desktop/src/renderer/components/Chat/ChatInterface/components/ModelPicker/hooks/useOpenAIOAuth/useOpenAIOAuth.ts
  • apps/desktop/src/renderer/routes/_authenticated/setup/providers/claude-code/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/setup/providers/codex/page.tsx
  • packages/chat/src/server/desktop/chat-service/chat-service.ts
  • packages/chat/src/server/desktop/router/router.ts

Comment on lines +389 to +405
try {
await loopback.start({
port: target.port,
path: target.path,
onCallback: (callbackUrl) => {
// Stash the callback URL so the renderer can consume it on its
// next poll. The renderer drives completion through the same
// completeOpenAIOAuth mutation as the manual-paste flow, so
// the dialog dismissal + navigation behavior stays consistent.
this.pendingOpenAIOAuthCallbackUrl = callbackUrl;
},
});
this.openAIOAuthLoopback = loopback;
} catch {
// Port unavailable or other bind failure — fall back to manual paste.
loopback.stop();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only silence expected bind failures here.

This bare catch turns any bug inside OpenAIOAuthLoopback.start() into a silent fallback, so the auto-capture path can break without surfacing anywhere. Restrict the silent path to expected bind errors and rethrow or at least log unexpected failures.

Suggested tightening
-			} catch {
-				// Port unavailable or other bind failure — fall back to manual paste.
-				loopback.stop();
-			}
+			} catch (error) {
+				loopback.stop();
+				const code = (error as NodeJS.ErrnoException | undefined)?.code;
+				if (code === "EADDRINUSE" || code === "EACCES") {
+					// Port unavailable — fall back to manual paste.
+					return result;
+				}
+				throw error;
+			}
🤖 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 `@packages/chat/src/server/desktop/chat-service/chat-service.ts` around lines
389 - 405, The current bare catch around loopback.start() swallows all errors;
change it to only silence expected bind failures (e.g., EADDRINUSE, EACCES) and
rethrow or log unexpected errors. Specifically, wrap the await
loopback.start(...) in try/catch, inspect the caught error.code (or instance)
inside the catch: if it's an address/bind error (EADDRINUSE, EACCES, etc.) call
loopback.stop() and proceed with the manual-paste fallback, otherwise log the
error via this.logger.error (or rethrow) so unexpected failures in
loopback.start() are surfaced; keep the assignment this.openAIOAuthLoopback =
loopback only on success.

Comment on lines +411 to +414
consumeOpenAIOAuthCallback(): { callbackUrl: string | null } {
const callbackUrl = this.pendingOpenAIOAuthCallbackUrl;
this.pendingOpenAIOAuthCallbackUrl = null;
return { callbackUrl };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't clear the captured callback before completion succeeds.

consumeOpenAIOAuthCallback() is destructive. If the renderer polls this value and completeOpenAIOAuth() then fails, the loopback redirect is gone and the user has to restart OAuth instead of retrying with the captured callback. Keep it until completion succeeds, or add an explicit ack/clear step after success.

🤖 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 `@packages/chat/src/server/desktop/chat-service/chat-service.ts` around lines
411 - 414, consumeOpenAIOAuthCallback currently clears
pendingOpenAIOAuthCallbackUrl immediately, which loses the callback if
completeOpenAIOAuth fails; change the behavior so the
pendingOpenAIOAuthCallbackUrl is not cleared by consumeOpenAIOAuthCallback
itself and instead is cleared only on successful completion (i.e., clear inside
completeOpenAIOAuth on success) or add an explicit ack method (e.g.,
ackOpenAIOAuthCallback) that the renderer calls after success; update references
to pendingOpenAIOAuthCallbackUrl, consumeOpenAIOAuthCallback(),
completeOpenAIOAuth(), and any polling code to use the new non-destructive
consume + explicit clear or clear-on-success flow.

AviPeltz added 6 commits May 5, 2026 20:30
Replace the standalone PNG and the placeholder bracket SVG with the
actual brand glyph extracted from packages/ui's preset-icons/superset.svg.
Render it inside a CSS-styled tile so all three provider tiles
(Superset / Claude / Codex) share identical 44x44 rounded-[10px] boxes,
and update the project page thumbnail to a rounded-square 48px tile with
a larger glyph.
Add per-worktree checkboxes to the adopt-worktrees onboarding step so
users can choose which worktrees to bring in instead of being forced into
all-or-nothing. New importExternalWorktrees tRPC mutation accepts an
explicit paths array and shares its filter logic with importAllWorktrees
through a new selectExternalWorktreesForImport helper.
…mport

Have getExternalWorktrees call selectExternalWorktreesForImport so the
list shown in the UI cannot drift from the rules applied during import.
@AviPeltz AviPeltz merged commit 5c35219 into main May 6, 2026
15 of 16 checks passed
@Kitenite Kitenite deleted the v2-onboarding-flow-review branch May 6, 2026 04:50
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 21, 2026
* feat(desktop): remove skip-all onboarding affordance

Per-step skipping is enough — drop the chrome-level "Skip onboarding"
button and the associated `skipAll` store action. Each step's
"Skip for now" link is unchanged.

* feat(desktop): unify onboarding step layout and stack providers

- Show Claude Code and Codex on the same providers page (no more
  tab toggle); each has its own selectable connection methods and
  Connect button, stacked vertically.
- Ensure every step renders both Continue (primary) and "Skip for
  now" (link) in a stable bottom column. Adds the missing skip on
  the gh-cli installed branch and the adopt-worktrees empty branch.

* feat(desktop): auto-capture OpenAI OAuth callback via loopback

- Add a localhost loopback HTTP server that binds to the redirect_uri
  port (1455) advertised in the OpenAI authorize URL. When the browser
  hits /auth/callback, the server passes the URL straight to
  completeOpenAIOAuth so the user no longer has to copy/paste it. Falls
  back silently to the existing manual paste flow if the port can't
  be bound.
- Tidy the providers page reconfigure UX: move Reconfigure/Cancel to a
  small inline link in the section header so the in-flow stack is
  just option cards + a single primary button instead of three stacked
  buttons.

* fix(desktop): route back to providers page after reconnect completes

Reconfigure flows previously got stuck on the OAuth subroute because
the navigate-back effect required wasAuthedOnMount to be false — true
for any reconnect.

- Drive both Claude and Codex back to /setup/providers from the OAuth
  hook's onAuthStateChange callback (fires whether the credential was
  fresh or refreshed). Drop the wasAuthedOnMount gating.
- Wire the OpenAI loopback through the renderer: the server stashes
  the callback URL, the renderer polls a new consumeOpenAIOAuthCallback
  query while the dialog is open and runs the existing
  completeOpenAIOAuth mutation, so the dialog dismissal +
  navigate-back path is the same for manual paste and auto-capture.
- Add onAuthStateChange parameter to useOpenAIOAuth (already existed
  on useAnthropicOAuth).

* fix(desktop): bind OAuth loopback to redirect_uri host, not hardcoded IPv4

parseLoopbackTargetFromAuthUrl now preserves the host from the redirect
URI (stripping IPv6 brackets) and threads it through to server.listen,
so an OpenAI client configured with [::1] or a system that resolves
localhost to IPv6 won't fail to bind.

* feat(desktop): use real app icon on connect-provider pages

Swap the inline SupersetIcon SVG (bracket marks rendered as text on a
flat circle) for the actual macOS app icon PNG, so the Connect Claude
Code and Connect Codex headers show the rounded-square brand mark
users recognize from the dock.

* style(desktop): round superset icon on connect pages to match provider logos

* style(desktop): scale superset icon so it fills the circular crop

* style(desktop): use inline SupersetIcon SVG on connect pages

The macOS app-icon PNG has rounded-square padding and a bottom shadow
baked in, which looked off when shoehorned into a circular badge next
to the Claude/Codex logos. Switch back to the SVG bracket mark on a
flat circular dark background — crisp at any size and visually
consistent with the provider icons.

* feat(desktop): use docs logo.png for connect provider pages

Add a copy of apps/docs/public/logo.png at apps/desktop/src/renderer/
assets/superset-logo.png so the desktop app has its own copy, and use
it as the Superset mark in Connect Claude Code and Connect Codex.
Renders at the logo's native rounded-square shape.

* style(desktop): square off provider logos to match superset icon shape

* style(desktop): match superset icon size and squared border container

* style(desktop): bump superset logo scale so brackets match codex/claude size

* style(desktop): leave superset logo at natural size, bump codex/claude inner icons

* style(desktop): use real superset brand glyph for setup tiles

Replace the standalone PNG and the placeholder bracket SVG with the
actual brand glyph extracted from packages/ui's preset-icons/superset.svg.
Render it inside a CSS-styled tile so all three provider tiles
(Superset / Claude / Codex) share identical 44x44 rounded-[10px] boxes,
and update the project page thumbnail to a rounded-square 48px tile with
a larger glyph.

* feat(desktop): selectable worktree import in onboarding

Add per-worktree checkboxes to the adopt-worktrees onboarding step so
users can choose which worktrees to bring in instead of being forced into
all-or-nothing. New importExternalWorktrees tRPC mutation accepts an
explicit paths array and shares its filter logic with importAllWorktrees
through a new selectExternalWorktreesForImport helper.

* refactor(desktop): share external-worktree filter between query and import

Have getExternalWorktrees call selectExternalWorktreesForImport so the
list shown in the UI cannot drift from the rules applied during import.
MocA-Love added a commit to MocA-Love/superset that referenced this pull request May 21, 2026
…o-v2

superset-sh#4113 remove skip-all onboarding affordance: integrate fork's
githubSyncService import alongside upstream's selectExternalWorktreesForImport
in workspaces/procedures/git-status.ts.

superset-sh#4115 default new users to v2 + v2 banner in v1:
- Adopt upstream's V2_CLOUD flag removal: collapse useIsV2CloudEnabled to
  a plain boolean (drop isRemoteV2Enabled) and update every callsite
  back to scalar destructure (TopBar, settings/{behavior,experimental,
  git,links,terminal}/page, SettingsSidebar/{GeneralSettings,SettingsSidebar}).
- ExperimentalSettings.tsx: drop isRemoteV2Enabled-gated 'Early access
  not enabled' note and Switch disabled prop; simplify track call.
- v2-local-override.ts: keep fork's IS_DEV strong default (dev installs
  land on v2) alongside upstream's hasPriorSupersetUsage probe
  (initialOptInV2 = IS_DEV ? true : !hasPriorSupersetUsage()).
- TopBar.tsx: integrate upstream's useWorkspaceSidebarStore reads with
  fork's existing chrome layout.
- useMigrateV1DataToV2/useMigrateV1DataToV2.ts: re-delete (DU; upstream
  attempted to revive via superset-sh#4115 vocabulary cleanup; cycle 38 rationale
  still applies — fork trpc lacks the required procedures).

superset-sh#4125 onboarding auto-skip drop, SetupButton design alignment: clean
cherry-pick.

superset-sh#4175 keep Skip-for-now visible during loading: clean cherry-pick.
Replaces v2Projects live query with imperative v2Project.list trpc
call; backfill list procedure (jwtProcedure variant) into fork's
v2-project router (mirrors get pattern using ctx.organizationIds
membership check).

superset-sh#4214 stable Adopt button + Adopt-all (skipped): touches v1
ImportWorkspacesPage/V1ImportModal which the fork has removed. The
upstream improvements (idempotent adopt-all guard, smarter skip-when-
already-running) have no shipping surface in this fork; pick up if and
when v1 import modal is reintroduced.

superset-sh#4093 re-add Settings to org dropdown for v1: clean cherry-pick.
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