Skip to content

refactor(desktop): add cloud profile management flow#432

Merged
mrcfps merged 8 commits intomainfrom
feat/desktop-cloud-profile-flow
Mar 23, 2026
Merged

refactor(desktop): add cloud profile management flow#432
mrcfps merged 8 commits intomainfrom
feat/desktop-cloud-profile-flow

Conversation

@mrcfps
Copy link
Copy Markdown
Contributor

@mrcfps mrcfps commented Mar 23, 2026

Summary

  • add desktop cloud profile management so the app can create, switch, connect, disconnect, and import multiple Nexu Cloud profiles from the desktop runtime
  • remove the local controller/web sign-in bootstrap so desktop opens without a local auth gate and keeps login/authorization in Nexu Cloud flows
  • layer repo, controller, and desktop env files for local desktop development and regenerate the controller/web API surfaces for the new desktop endpoints

Notes

  • the current working tree still has two uncommitted lockfile-only changes, so they are not included in this PR

Summary by CodeRabbit

  • New Features

    • Cloud profile management UI: create, update, delete, import/export profiles; per-profile connect/disconnect/select and live status polling.
    • Richer cloud status display with active profile, per-profile model counts and connection details.
  • Removals

    • Authentication UI and desktop session bootstrap/recovery removed.
    • Nexu Cloud / Link environment and build/runtime config entries removed; Cloud/Link no longer shown in build info.

mrcfps added 6 commits March 23, 2026 19:32
Use the cloud profile page as the single source of truth for cloud and link endpoints so desktop and controller no longer depend on manual env configuration.
Make the desktop workspace accessible without a local auth gate and keep account login flows in Nexu Cloud instead of the controller/web shell.
Load root, controller, and desktop env files in order so desktop local development can share repo-wide defaults while allowing app-specific overrides.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Removed local auth/session handling from controller and desktop. Introduced multi-profile desktop cloud support in controller with persistent cloud-profiles, new profile management APIs, desktop IPC and SDK bindings, a desktop UI for profile management, and removed static Nexu cloud/link env/build-config usage.

Changes

Cohort / File(s) Summary
Env & Runtime Config
apps/controller/.env.example, apps/controller/src/app/env.ts, apps/desktop/shared/runtime-config.ts, apps/desktop/main/runtime/manifests.ts, apps/desktop/scripts/dist-mac.mjs
Removed NEXU_CLOUD_URL/NEXU_LINK_URL from env/build-config handling and exports; desktop runtime no longer exposes cloud/link URLs from env/build-config.
Controller Auth Removal
apps/controller/src/routes/auth-routes.ts, apps/controller/src/routes/misc-compat-routes.ts, apps/controller/src/app/create-app.ts, apps/controller/src/services/local-user-service.ts
Deleted local auth routes, session APIs, and POST /api/auth/check-email; removed auth route registration.
Cloud Profile APIs & OpenAPI
apps/controller/openapi.json, packages/shared/src/schemas/provider.ts, apps/web/lib/api/types.gen.ts, apps/web/lib/api/sdk.gen.ts
Added /api/internal/desktop/cloud-profile/* endpoints and expanded cloud-status/refresh schemas to include per-profile fields (cloudUrl, linkUrl, activeProfileName, profiles[]); updated OpenAPI/types/SDK accordingly; removed auth check-email types.
Controller Store & Services
apps/controller/src/store/nexu-config-store.ts, apps/controller/src/store/schemas.ts, apps/controller/src/services/desktop-local-service.ts
Added persistent cloud-profiles.json and profile CRUD/switch/connect/disconnect/import APIs; migrated cloud session state to profile-scoped storage and exported new NexuConfigStore and DesktopLocalService methods.
Desktop Main & IPC
apps/desktop/main/desktop-bootstrap.ts, apps/desktop/main/index.ts, apps/desktop/main/ipc.ts, apps/desktop/shared/host.ts, apps/desktop/scripts/dev-env.sh
Removed desktop auth bootstrap and related hooks; replaced desktop:ensure-auth-session with typed cloud-profile IPC channels and retrying fetch helper; dev script now sources root .env.
Desktop UI
apps/desktop/src/pages/cloud-profile-page.tsx, apps/desktop/src/main.tsx, apps/desktop/src/components/desktop-shell.tsx, apps/desktop/src/runtime-page.css
Added CloudProfilePage, navigation integration, new UI/CSS for cloud profiles; removed auth-session-restored handling and cloud/link build-info display.
Web App Auth Removal & Layouts
apps/web/src/pages/auth.tsx, apps/web/src/app.tsx, apps/web/src/layouts/auth-layout.tsx, apps/web/src/pages/*
Removed AuthPage and its routes; updated AuthLayout redirect behavior and pages to stop linking to /auth.
Tests & Diagnostics
apps/controller/tests/*, apps/desktop/main/diagnostics-export.ts
Extended tests to cover profile flows and added runtime writers; removed cloud/link from diagnostics export.
Client SDK / Types
apps/web/lib/api/types.gen.ts, apps/web/lib/api/sdk.gen.ts, packages/shared/src/schemas/provider.ts
Added types/schemas and SDK helpers for cloud-profile endpoints; updated cloud-status types and removed check-email SDK/type.
Docs / Specs
specs/*
Updated design/spec docs to reflect cloud profile sourcing of cloud/link endpoints and adjusted crash-reporting note.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Desktop as Desktop App
    participant IPC as IPC Bridge
    participant Controller as Controller Service
    participant Store as NexuConfigStore
    participant Cloud as External Cloud

    User->>Desktop: Click "Connect" on profile
    Desktop->>IPC: desktop:connect-cloud-profile({name})
    IPC->>Controller: POST /api/internal/desktop/cloud-profile/connect
    Controller->>Store: connectDesktopCloudProfile(name)
    Store->>Cloud: initiate auth flow (cloudUrl)
    Cloud-->>Store: returns browserUrl / auth link
    Store-->>Controller: { status, browserUrl }
    Controller-->>IPC: { status, browserUrl, configPushed }
    IPC-->>Desktop: show browser link + polling state

    loop poll every 2s
      Desktop->>IPC: desktop:get-cloud-status()
      IPC->>Controller: GET /api/internal/desktop/cloud-status
      Controller->>Store: getDesktopCloudStatus()
      Store-->>Controller: status with profiles[]
      Controller-->>IPC: status
      IPC-->>Desktop: update UI
    end
Loading
sequenceDiagram
    actor User
    participant Desktop as Desktop App
    participant IPC as IPC Bridge
    participant Controller as Controller Service
    participant Store as NexuConfigStore

    User->>Desktop: Create new profile
    Desktop->>IPC: desktop:create-cloud-profile({profile})
    IPC->>Controller: POST /api/internal/desktop/cloud-profile/create
    Controller->>Store: createDesktopCloudProfile(profile)
    Store->>Store: validate & persist cloud-profiles.json
    Store-->>Controller: updated status
    Controller-->>IPC: { ok: true, status..., configPushed }
    IPC-->>Desktop: refresh list / success message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • lefarcen
  • anthhub

Poem

🐰
I nibbled through routes and hopped the fence,
Auth crumbs gone, profiles now commence.
Many clouds to choose, each with its own door,
I dance between profiles and ask for more.
Hooray — the rabbit claps for config galore!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provided covers the summary and notes but is missing key sections from the template including What, Why, How, Affected areas checklist, and Checklist items. Add missing template sections: expand the one-liner explanation under 'What', add motivation/related issues under 'Why', explain key design decisions under 'How', check the 'Affected areas' checkboxes, and confirm the completion checklist items before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding cloud profile management flow to the desktop app, which is accurately reflected across all the changed files.

✏️ 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 feat/desktop-cloud-profile-flow

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.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 23, 2026

Deploying nexu-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 43febd7
Status: ✅  Deploy successful!
Preview URL: https://de6909db.nexu-docs.pages.dev
Branch Preview URL: https://feat-desktop-cloud-profile-f.nexu-docs.pages.dev

View logs

@mrcfps mrcfps changed the title feat(desktop): add cloud profile management flow refactor(desktop): add cloud profile management flow Mar 23, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (4)
apps/web/src/pages/slack-claim.tsx (2)

532-539: ⚠️ Potential issue | 🟡 Minor

"Use different account" does not sign out the current user.

The link navigates to / while the user remains authenticated. To actually use a different account, the user must first sign out. Consider either:

  1. Calling authClient.signOut() before navigation (like invite.tsx does), or
  2. Renaming the link to clarify it goes to the landing page without switching accounts.
🔧 Proposed fix to sign out before navigating
-              <Link
-                to="/"
+              <button
+                type="button"
+                onClick={async () => {
+                  await authClient.signOut();
+                  window.location.href = `/claim?token=${claimKey}`;
+                }}
                 className="text-[13px] text-text-muted hover:text-text-secondary transition-colors"
               >
                 {t("claim.useDifferentAccount")}
-              </Link>
+              </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/slack-claim.tsx` around lines 532 - 539, The "Use
different account" Link in slack-claim.tsx only navigates to "/" but doesn't
sign the user out; update the Link to perform a sign-out first (call
authClient.signOut() like invite.tsx does) then navigate to "/" (e.g., replace
the Link with a button or onClick handler that awaits authClient.signOut() and
then routes to "/"), or if you prefer not to change behavior, rename the
displayed text key t("claim.useDifferentAccount") to clarify it only goes to the
landing page; reference authClient.signOut and the current Link rendering in
slack-claim.tsx when making the change.

416-439: ⚠️ Potential issue | 🟠 Major

The return-to-claim flow is incomplete and will break the claim process for unauthenticated users.

The code saves CLAIM_RETURN_KEY to sessionStorage before navigating to /, but WelcomePage and the authentication flow have no logic to read this key and redirect authenticated users back to /claim?token=... to complete the claim. The previous /auth?returnTo=... pattern handled this automatically; the new landing page approach requires explicit handling.

Implement detection of CLAIM_RETURN_KEY in sessionStorage after successful authentication and redirect back to the claim page with the original token to preserve the claim workflow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/slack-claim.tsx` around lines 416 - 439, The landing/auth
flow must read CLAIM_RETURN_KEY from sessionStorage after successful sign-in and
redirect the user to complete the claim; add logic in the WelcomePage (or the
post-auth callback/handler used after login) to check
sessionStorage.getItem(CLAIM_RETURN_KEY), parse the stored claim token
(claimKey), clear the key, and programmatically navigate to
`/claim?token=${claimKey}` (use your router's navigate/replace) so authenticated
users are sent back to Claim flow; ensure this runs once (e.g., in a useEffect
or onAuthSuccess handler) and only when the user is authenticated to avoid
breaking other flows.
apps/web/src/pages/feishu-bind.tsx (2)

191-208: ⚠️ Potential issue | 🟠 Major

Authentication flow loses Feishu binding context.

The "Create account" and "Log in" links send unauthenticated users to /, which loads WelcomePage. However, WelcomePage does not support a returnTo query parameter—it redirects authenticated users directly to /workspace, losing the ws and bot params needed to complete the Feishu bind.

Users arriving from Feishu will be unable to return to the binding flow after authentication. Consider either preserving the bind context through the auth flow or providing an explicit path back to Feishu binding after successful login.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/feishu-bind.tsx` around lines 191 - 208, The auth links on
the Feishu bind page currently point to "/" which loses the ws and bot query
params needed to finish binding; update the two Link elements in feishu-bind.tsx
(the "Create account" and "Log in" Link nodes) to preserve the current bind
context by appending the existing query string (ws and bot) or by routing to an
auth path that accepts a returnTo (e.g.,
/auth/login?returnTo=/feishu-bind{currentQuery}) so WelcomePage's redirect to
/workspace doesn't drop the bind params; use the page's location/search (via
useLocation) or build the URL from ws and bot params and ensure the target route
will forward returnTo after successful auth so the binding flow can resume.

264-271: ⚠️ Potential issue | 🟡 Minor

"Use a different account" link doesn't enable account switching.

This link is shown to authenticated users who want to bind a different account. Navigating to / redirects back to the workspace because the page interprets authentication as "setup complete"—it doesn't sign out or present an account switcher.

Add await authClient.signOut() before navigation, following the pattern used in invite.tsx (line 155):

Suggested fix
<div className="text-center mt-4">
  <button
    type="button"
    onClick={async () => {
      await authClient.signOut();
      navigate("/");
    }}
    className="text-[13px] text-text-muted hover:text-text-secondary transition-colors"
  >
    {t("claim.useDifferentAccount")}
  </button>
</div>

Note: slack-claim.tsx (line 537) has the same issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/pages/feishu-bind.tsx` around lines 264 - 271, The "Use a
different account" Link currently just navigates to "/" without signing the user
out, so add an async click handler that calls await authClient.signOut() before
performing the navigation (follow the pattern used in invite.tsx where
authClient.signOut() is awaited prior to routing); update the Link in
feishu-bind.tsx to prevent default navigation and invoke signOut then route to
"/" (and apply the same change to slack-claim.tsx at the analogous Link). Ensure
you reference authClient.signOut() and the Link element (or its onClick handler)
when making the change.
🧹 Nitpick comments (8)
apps/controller/src/store/schemas.ts (1)

157-166: Consider adding schema migration/preprocessing for backward compatibility.

The cloudProfilesFileSchema requires schemaVersion as a positive integer, but if users have existing cloud profile files without this field (or from a previous format), schema validation will fail. Unlike nexuConfigSchema (lines 105-145) which uses z.preprocess() to normalize missing fields, this schema has no migration path.

If cloud-profiles.json is a new file that didn't exist before this PR, this is fine. However, if there's any possibility of existing files, consider adding preprocessing similar to nexuConfigSchema.

Example preprocessing pattern
export const cloudProfilesFileSchema = z.preprocess((input) => {
  if (typeof input !== "object" || input === null) {
    return input;
  }
  const candidate = input as Record<string, unknown>;
  return {
    schemaVersion: typeof candidate.schemaVersion === "number" ? candidate.schemaVersion : 1,
    profiles: Array.isArray(candidate.profiles) ? candidate.profiles : [],
  };
}, z.object({
  schemaVersion: z.number().int().positive(),
  profiles: z.array(cloudProfileEntrySchema).default([]),
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/store/schemas.ts` around lines 157 - 166, The
cloudProfilesFileSchema currently requires schemaVersion and will reject
older/absent fields; add a z.preprocess like nexuConfigSchema to normalize
legacy inputs: in the preprocess handler for cloudProfilesFileSchema detect
non-object or missing schemaVersion/profiles and return an object with
schemaVersion defaulting to 1 and profiles defaulting to [], then validate
against the existing z.object (which uses cloudProfileEntrySchema); this
preserves backward compatibility for existing cloud-profiles.json files while
keeping the same shape for new files.
apps/desktop/src/pages/cloud-profile-page.tsx (2)

14-34: Consider importing types from shared schema.

The CloudStatus type duplicates the shape defined in @nexu/shared. While the renderer may not directly import Zod schemas, you could use z.infer<typeof cloudStatusResponseSchema> if the shared package exports the inferred types, improving type safety and reducing duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/pages/cloud-profile-page.tsx` around lines 14 - 34, Replace
the locally-declared CloudStatus type with the shared inferred type from
`@nexu/shared`: import the exported inferred type (e.g., z.infer<typeof
cloudStatusResponseSchema> or an exported alias) instead of duplicating the
shape; update any references to CloudStatus in this file (e.g., props, state, or
function signatures) to use the imported type name and remove the local type
declaration to keep types consistent and DRY with the shared schema.

660-676: Simplify duplicate conditional branches.

Both branches of the ternary render identical JSX. The cloudMessage check isn't affecting the output.

♻️ Proposed simplification
-      {cloudMessage ? (
-        <p className={`runtime-cloud-message is-${statusBannerTone}`}>
-          <span
-            className={`runtime-cloud-message-dot is-${statusBannerTone}`}
-            aria-hidden="true"
-          />
-          <span>{statusBannerMessage}</span>
-        </p>
-      ) : (
-        <p className={`runtime-cloud-message is-${statusBannerTone}`}>
-          <span
-            className={`runtime-cloud-message-dot is-${statusBannerTone}`}
-            aria-hidden="true"
-          />
-          <span>{statusBannerMessage}</span>
-        </p>
-      )}
+      <p className={`runtime-cloud-message is-${statusBannerTone}`}>
+        <span
+          className={`runtime-cloud-message-dot is-${statusBannerTone}`}
+          aria-hidden="true"
+        />
+        <span>{statusBannerMessage}</span>
+      </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/pages/cloud-profile-page.tsx` around lines 660 - 676, The
conditional using cloudMessage renders identical JSX in both branches; replace
the ternary with a single unconditional render of the paragraph using the same
classes and values (referencing cloudMessage only if needed elsewhere), e.g.
render the <p> that uses statusBannerTone and statusBannerMessage and the inner
span with class runtime-cloud-message-dot once instead of duplicating it in the
cloudMessage ? ... : ... branches; remove the redundant conditional and keep use
of statusBannerTone/statusBannerMessage to preserve current behavior.
apps/controller/src/routes/desktop-compat-routes.ts (1)

96-103: Consider calling ensureValidDefaultModel() for consistency.

The cloud-profile/connect handler does not call modelProviderService.ensureValidDefaultModel() after the operation, unlike all other mutation handlers (create, update, delete, disconnect, select, import) which do. If connecting a profile can affect the available models, this inconsistency could lead to an invalid default model state.

♻️ Proposed fix for consistency
     async (c) => {
       const body = c.req.valid("json");
       const result = await container.desktopLocalService.connectCloudProfile(
         body.name,
       );
+      await container.modelProviderService.ensureValidDefaultModel();
       const { configPushed } = await container.openclawSyncService.syncAll();
       return c.json({ ...result, configPushed }, 200);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/routes/desktop-compat-routes.ts` around lines 96 - 103,
The cloud-profile/connect route handler currently calls
container.desktopLocalService.connectCloudProfile(...) and then
container.openclawSyncService.syncAll(), but unlike other mutation handlers it
does not call modelProviderService.ensureValidDefaultModel(); update the async
handler to invoke container.modelProviderService.ensureValidDefaultModel() after
the syncAll() (and before returning the JSON response) so the default model
state is validated consistently after connecting a profile.
apps/desktop/src/lib/host-api.ts (1)

97-101: Extract a shared DesktopCloudProfileInput type.

The same payload shape is redeclared three times here. Because this sits on a renderer↔host boundary, duplicating it makes future field/nullability changes drift silently.

♻️ Suggested refactor
+type DesktopCloudProfileInput = {
+  name: string;
+  cloudUrl: string;
+  linkUrl: string;
+};

-export async function createCloudProfile(profile: {
-  name: string;
-  cloudUrl: string;
-  linkUrl: string;
-}) {
+export async function createCloudProfile(profile: DesktopCloudProfileInput) {
   return getHostBridge().invoke("desktop:create-cloud-profile", { profile });
 }

 export async function importCloudProfiles(
-  profiles: Array<{ name: string; cloudUrl: string; linkUrl: string }>,
+  profiles: DesktopCloudProfileInput[],
 ) {
   return getHostBridge().invoke("desktop:import-cloud-profiles", { profiles });
 }

 export async function updateCloudProfile(
   previousName: string,
-  profile: { name: string; cloudUrl: string; linkUrl: string },
+  profile: DesktopCloudProfileInput,
 ) {
   return getHostBridge().invoke("desktop:update-cloud-profile", {
     previousName,
     profile,

Also applies to: 117-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/host-api.ts` around lines 97 - 101, Extract a shared
type alias (e.g., DesktopCloudProfileInput) defining { name: string; cloudUrl:
string; linkUrl: string } and replace the inline object literal parameter types
in createCloudProfile and the other functions that redeclare the same shape
(referenced in the diff around createCloudProfile and the block at lines
117-125) to use that alias; update any related import/exports so the
renderer↔host boundary uses the single DesktopCloudProfileInput type to prevent
drift when fields/nullability change.
apps/desktop/shared/host.ts (1)

148-347: Consider extracting a shared CloudProfileStatus type to reduce duplication.

The result types for desktop:create-cloud-profile, desktop:disconnect-cloud-profile, desktop:switch-cloud-profile, etc. all contain an identical nested structure (lines 162-172 repeated ~7 times). Extracting a shared type would reduce maintenance burden.

♻️ Optional: Extract shared type
export type CloudProfileStatus = {
  connected: boolean;
  polling?: boolean;
  userName?: string | null;
  userEmail?: string | null;
  connectedAt?: string | null;
  models?: Array<{
    id: string;
    name: string;
    provider?: string;
  }>;
  cloudUrl: string;
  linkUrl: string | null;
  activeProfileName: string;
  profiles: Array<{
    name: string;
    cloudUrl: string;
    linkUrl: string;
    connected: boolean;
    polling?: boolean;
    userName?: string | null;
    userEmail?: string | null;
    connectedAt?: string | null;
    modelCount: number;
  }>;
};

// Then use in result maps:
"desktop:get-cloud-status": CloudProfileStatus;
"desktop:create-cloud-profile": CloudProfileStatus & { ok: boolean; configPushed: boolean };
// etc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/shared/host.ts` around lines 148 - 347, The types for responses
like "desktop:get-cloud-status", "desktop:create-cloud-profile",
"desktop:disconnect-cloud-profile", "desktop:switch-cloud-profile",
"desktop:import-cloud-profiles", "desktop:update-cloud-profile", and
"desktop:delete-cloud-profile" duplicate the same nested structure; extract that
shared shape into a new exported type (e.g., CloudProfileStatus) capturing
connected, polling, userName, userEmail, connectedAt, models, cloudUrl, linkUrl,
activeProfileName, and profiles, then replace the repeated inline definitions
with CloudProfileStatus and compose any extra fields (e.g., ok, configPushed,
browserUrl, error) by intersecting or extending CloudProfileStatus in the
existing result map entries (refer to the existing keys
"desktop:get-cloud-status" and "desktop:create-cloud-profile" to guide where to
swap to CloudProfileStatus).
apps/controller/src/store/nexu-config-store.ts (2)

344-382: Dual-write pattern maintains backward compatibility but increases consistency burden.

writeActiveDesktopCloudState writes identical data to both desktop.cloud and desktop.cloudSessions[activeProfileName]. This supports backward compatibility but creates a consistency responsibility. If either location gets out of sync (e.g., due to a future code path that only updates one), consumers could see inconsistent state.

Consider documenting this pattern with a comment explaining the dual-write rationale and noting that desktop.cloud is the primary read location while cloudSessions provides per-profile persistence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/store/nexu-config-store.ts` around lines 344 - 382, Add a
concise comment in writeActiveDesktopCloudState explaining the intentional
dual-write to desktop.cloud and desktop.cloudSessions[activeProfileName]: state
that this mirrors data for backward compatibility, declare which location
(desktop.cloud) is the primary read location and that cloudSessions provides
per-profile persistence, and note the consistency burden (ensure any future
updates must update both locations) so maintainers know why both are updated and
to keep them in sync.

140-156: Consider validating model array elements for defensive parsing.

The function casts cloud.models without validating that array elements conform to the expected shape { id: string; name: string; provider?: string }. If persisted data is malformed (e.g., due to schema migration or file corruption), downstream code may encounter unexpected property accesses.

🛡️ Optional: Add element validation
+function isValidCloudModel(item: unknown): item is { id: string; name: string; provider?: string } {
+  return (
+    typeof item === "object" &&
+    item !== null &&
+    typeof (item as Record<string, unknown>).id === "string" &&
+    typeof (item as Record<string, unknown>).name === "string"
+  );
+}
+
 function normalizeDesktopCloudState(
   cloud: Record<string, unknown> | null,
 ): DesktopCloudState {
   return {
     connected: cloud?.connected === true,
     polling: cloud?.polling === true,
     userName: typeof cloud?.userName === "string" ? cloud.userName : null,
     userEmail: typeof cloud?.userEmail === "string" ? cloud.userEmail : null,
     connectedAt:
       typeof cloud?.connectedAt === "string" ? cloud.connectedAt : null,
     linkUrl: typeof cloud?.linkUrl === "string" ? cloud.linkUrl : undefined,
     apiKey: typeof cloud?.apiKey === "string" ? cloud.apiKey : undefined,
-    models: Array.isArray(cloud?.models)
-      ? (cloud.models as Array<{ id: string; name: string; provider?: string }>)
-      : [],
+    models: Array.isArray(cloud?.models)
+      ? cloud.models.filter(isValidCloudModel)
+      : [],
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/src/store/nexu-config-store.ts` around lines 140 - 156, The
normalizeDesktopCloudState function currently casts cloud.models directly, which
can let malformed entries through; update it to defensively validate each
element of cloud.models (in normalizeDesktopCloudState) by checking that each
item is an object with string id and name and optional string provider, then
map/filter the array to a safe Array<{id: string; name: string; provider?:
string}> (or return [] if none valid); do not use a direct cast of cloud.models
— instead iterate, validate types for id/name/provider, and build the normalized
models array before returning it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/controller/openapi.json`:
- Around line 1678-1685: The cloudUrl and linkUrl schema definitions currently
only enforce non-empty strings; update both property objects for cloudUrl and
linkUrl to include "format": "uri" so they validate as URIs (i.e., add a
"format": "uri" field alongside "type": "string" and "minLength": 1). Apply the
same change to every other occurrence of these properties in the OpenAPI schema
(the other cloudUrl/linkUrl blocks referenced in the review) so all instances
enforce URI format consistently.
- Around line 1373-1390: Several cloud-profile routes declare JSON request
bodies but omit request.body.required, making generated OpenAPI/SDK treat them
optional; update each route definition that uses cloudProfileConnectBodySchema
(cloud-profile/create, cloud-profile/update, cloud-profile/delete,
cloud-profile/disconnect, cloud-profile/select, cloud-profiles/import) to add
request: { body: { required: true, content: { "application/json": { schema: ...
} } } } so the spec matches the runtime c.req.valid("json") expectation. Also
adjust cloudProfileSchema to validate URLs at runtime by replacing the plain
string validators for cloudUrl and linkUrl with Zod's .url() (e.g., change
cloudUrl and linkUrl from z.string().min(1) to z.string().url()).

In `@apps/controller/src/store/nexu-config-store.ts`:
- Around line 1549-1570: The code currently overwrites cached models when
fetchDesktopCloudModels returns null; update the logic in the block that uses
getConfig/readDesktopCloud so nextModels is assigned to refreshedModels if
present, otherwise fall back to the previously cached switchedCloud.models (and
finally an empty array): i.e., change the assignment that now does nextModels =
refreshedModels ?? [] to preserve switchedCloud.models on fetch failure (ensure
this updated nextModels is what you pass into setDesktopCloudState.models);
reference functions/vars: getConfig, readDesktopCloud, fetchDesktopCloudModels,
switchedCloud, nextModels, nextProfile, setDesktopCloudState.

In `@apps/controller/tests/openclaw-sync.test.ts`:
- Around line 29-30: Remove the unused nexuCloudUrl and nexuLinkUrl properties
from the test environment object used in the openclaw sync test so the object
matches the actual ControllerEnv shape; delete the nexuCloudUrl and nexuLinkUrl
entries (symbols: nexuCloudUrl, nexuLinkUrl) from the test env definition in
openclaw-sync test and run TypeScript type checks to ensure no other references
remain.

In `@apps/web/src/layouts/auth-layout.tsx`:
- Around line 1-5: AuthLayout currently renders Outlet without enforcing
authentication, leaving workspace routes (wrapped in app.tsx) reachable by
unauthenticated users; restore an auth gate by checking the session/user before
rendering in AuthLayout (use the existing session hook or loader) and redirect
to login or return a 401 if absent; also add the same session.user validation in
InviteGuardLayout and WorkspaceLayout where they render children, and ensure
server-side handlers defined in create-app.ts (e.g., /api/v1/sessions,
/api/v1/me) have authentication middleware or explicit checks to reject requests
without valid auth.

---

Outside diff comments:
In `@apps/web/src/pages/feishu-bind.tsx`:
- Around line 191-208: The auth links on the Feishu bind page currently point to
"/" which loses the ws and bot query params needed to finish binding; update the
two Link elements in feishu-bind.tsx (the "Create account" and "Log in" Link
nodes) to preserve the current bind context by appending the existing query
string (ws and bot) or by routing to an auth path that accepts a returnTo (e.g.,
/auth/login?returnTo=/feishu-bind{currentQuery}) so WelcomePage's redirect to
/workspace doesn't drop the bind params; use the page's location/search (via
useLocation) or build the URL from ws and bot params and ensure the target route
will forward returnTo after successful auth so the binding flow can resume.
- Around line 264-271: The "Use a different account" Link currently just
navigates to "/" without signing the user out, so add an async click handler
that calls await authClient.signOut() before performing the navigation (follow
the pattern used in invite.tsx where authClient.signOut() is awaited prior to
routing); update the Link in feishu-bind.tsx to prevent default navigation and
invoke signOut then route to "/" (and apply the same change to slack-claim.tsx
at the analogous Link). Ensure you reference authClient.signOut() and the Link
element (or its onClick handler) when making the change.

In `@apps/web/src/pages/slack-claim.tsx`:
- Around line 532-539: The "Use different account" Link in slack-claim.tsx only
navigates to "/" but doesn't sign the user out; update the Link to perform a
sign-out first (call authClient.signOut() like invite.tsx does) then navigate to
"/" (e.g., replace the Link with a button or onClick handler that awaits
authClient.signOut() and then routes to "/"), or if you prefer not to change
behavior, rename the displayed text key t("claim.useDifferentAccount") to
clarify it only goes to the landing page; reference authClient.signOut and the
current Link rendering in slack-claim.tsx when making the change.
- Around line 416-439: The landing/auth flow must read CLAIM_RETURN_KEY from
sessionStorage after successful sign-in and redirect the user to complete the
claim; add logic in the WelcomePage (or the post-auth callback/handler used
after login) to check sessionStorage.getItem(CLAIM_RETURN_KEY), parse the stored
claim token (claimKey), clear the key, and programmatically navigate to
`/claim?token=${claimKey}` (use your router's navigate/replace) so authenticated
users are sent back to Claim flow; ensure this runs once (e.g., in a useEffect
or onAuthSuccess handler) and only when the user is authenticated to avoid
breaking other flows.

---

Nitpick comments:
In `@apps/controller/src/routes/desktop-compat-routes.ts`:
- Around line 96-103: The cloud-profile/connect route handler currently calls
container.desktopLocalService.connectCloudProfile(...) and then
container.openclawSyncService.syncAll(), but unlike other mutation handlers it
does not call modelProviderService.ensureValidDefaultModel(); update the async
handler to invoke container.modelProviderService.ensureValidDefaultModel() after
the syncAll() (and before returning the JSON response) so the default model
state is validated consistently after connecting a profile.

In `@apps/controller/src/store/nexu-config-store.ts`:
- Around line 344-382: Add a concise comment in writeActiveDesktopCloudState
explaining the intentional dual-write to desktop.cloud and
desktop.cloudSessions[activeProfileName]: state that this mirrors data for
backward compatibility, declare which location (desktop.cloud) is the primary
read location and that cloudSessions provides per-profile persistence, and note
the consistency burden (ensure any future updates must update both locations) so
maintainers know why both are updated and to keep them in sync.
- Around line 140-156: The normalizeDesktopCloudState function currently casts
cloud.models directly, which can let malformed entries through; update it to
defensively validate each element of cloud.models (in
normalizeDesktopCloudState) by checking that each item is an object with string
id and name and optional string provider, then map/filter the array to a safe
Array<{id: string; name: string; provider?: string}> (or return [] if none
valid); do not use a direct cast of cloud.models — instead iterate, validate
types for id/name/provider, and build the normalized models array before
returning it.

In `@apps/controller/src/store/schemas.ts`:
- Around line 157-166: The cloudProfilesFileSchema currently requires
schemaVersion and will reject older/absent fields; add a z.preprocess like
nexuConfigSchema to normalize legacy inputs: in the preprocess handler for
cloudProfilesFileSchema detect non-object or missing schemaVersion/profiles and
return an object with schemaVersion defaulting to 1 and profiles defaulting to
[], then validate against the existing z.object (which uses
cloudProfileEntrySchema); this preserves backward compatibility for existing
cloud-profiles.json files while keeping the same shape for new files.

In `@apps/desktop/shared/host.ts`:
- Around line 148-347: The types for responses like "desktop:get-cloud-status",
"desktop:create-cloud-profile", "desktop:disconnect-cloud-profile",
"desktop:switch-cloud-profile", "desktop:import-cloud-profiles",
"desktop:update-cloud-profile", and "desktop:delete-cloud-profile" duplicate the
same nested structure; extract that shared shape into a new exported type (e.g.,
CloudProfileStatus) capturing connected, polling, userName, userEmail,
connectedAt, models, cloudUrl, linkUrl, activeProfileName, and profiles, then
replace the repeated inline definitions with CloudProfileStatus and compose any
extra fields (e.g., ok, configPushed, browserUrl, error) by intersecting or
extending CloudProfileStatus in the existing result map entries (refer to the
existing keys "desktop:get-cloud-status" and "desktop:create-cloud-profile" to
guide where to swap to CloudProfileStatus).

In `@apps/desktop/src/lib/host-api.ts`:
- Around line 97-101: Extract a shared type alias (e.g.,
DesktopCloudProfileInput) defining { name: string; cloudUrl: string; linkUrl:
string } and replace the inline object literal parameter types in
createCloudProfile and the other functions that redeclare the same shape
(referenced in the diff around createCloudProfile and the block at lines
117-125) to use that alias; update any related import/exports so the
renderer↔host boundary uses the single DesktopCloudProfileInput type to prevent
drift when fields/nullability change.

In `@apps/desktop/src/pages/cloud-profile-page.tsx`:
- Around line 14-34: Replace the locally-declared CloudStatus type with the
shared inferred type from `@nexu/shared`: import the exported inferred type (e.g.,
z.infer<typeof cloudStatusResponseSchema> or an exported alias) instead of
duplicating the shape; update any references to CloudStatus in this file (e.g.,
props, state, or function signatures) to use the imported type name and remove
the local type declaration to keep types consistent and DRY with the shared
schema.
- Around line 660-676: The conditional using cloudMessage renders identical JSX
in both branches; replace the ternary with a single unconditional render of the
paragraph using the same classes and values (referencing cloudMessage only if
needed elsewhere), e.g. render the <p> that uses statusBannerTone and
statusBannerMessage and the inner span with class runtime-cloud-message-dot once
instead of duplicating it in the cloudMessage ? ... : ... branches; remove the
redundant conditional and keep use of statusBannerTone/statusBannerMessage to
preserve current behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ddb2345b-77b4-40e3-8d2c-7f65c063be5f

📥 Commits

Reviewing files that changed from the base of the PR and between b0a28e0 and f66e08e.

📒 Files selected for processing (40)
  • apps/controller/.env.example
  • apps/controller/openapi.json
  • apps/controller/src/app/create-app.ts
  • apps/controller/src/app/env.ts
  • apps/controller/src/routes/auth-routes.ts
  • apps/controller/src/routes/desktop-compat-routes.ts
  • apps/controller/src/routes/misc-compat-routes.ts
  • apps/controller/src/services/desktop-local-service.ts
  • apps/controller/src/services/local-user-service.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/controller/src/store/schemas.ts
  • apps/controller/tests/nexu-config-store.test.ts
  • apps/controller/tests/openclaw-sync.test.ts
  • apps/controller/tests/route-compat.test.ts
  • apps/desktop/main/desktop-bootstrap.ts
  • apps/desktop/main/diagnostics-export.ts
  • apps/desktop/main/index.ts
  • apps/desktop/main/ipc.ts
  • apps/desktop/main/runtime/manifests.ts
  • apps/desktop/scripts/dev-env.sh
  • apps/desktop/scripts/dist-mac.mjs
  • apps/desktop/shared/host.ts
  • apps/desktop/shared/runtime-config.ts
  • apps/desktop/src/components/desktop-shell.tsx
  • apps/desktop/src/lib/host-api.ts
  • apps/desktop/src/main.tsx
  • apps/desktop/src/pages/cloud-profile-page.tsx
  • apps/desktop/src/runtime-page.css
  • apps/web/lib/api/sdk.gen.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/app.tsx
  • apps/web/src/layouts/auth-layout.tsx
  • apps/web/src/pages/auth.tsx
  • apps/web/src/pages/feishu-bind.tsx
  • apps/web/src/pages/invite.tsx
  • apps/web/src/pages/slack-claim.tsx
  • packages/shared/src/schemas/provider.ts
  • specs/change/20260317-desktop-crash-reporting/spec.md
  • specs/designs/desktop-cloud-connection.md
  • specs/designs/nexu-link-integration.md
💤 Files with no reviewable changes (12)
  • apps/controller/src/app/create-app.ts
  • apps/desktop/main/diagnostics-export.ts
  • apps/desktop/main/runtime/manifests.ts
  • apps/controller/.env.example
  • apps/controller/src/routes/misc-compat-routes.ts
  • apps/desktop/scripts/dist-mac.mjs
  • apps/web/src/app.tsx
  • apps/controller/src/app/env.ts
  • apps/controller/src/routes/auth-routes.ts
  • apps/web/src/pages/auth.tsx
  • apps/desktop/shared/runtime-config.ts
  • apps/desktop/main/desktop-bootstrap.ts

Comment thread apps/controller/openapi.json
Comment thread apps/controller/openapi.json
Comment thread apps/controller/src/store/nexu-config-store.ts
Comment thread apps/controller/tests/openclaw-sync.test.ts Outdated
Comment thread apps/web/src/layouts/auth-layout.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/controller/openapi.json (1)

1267-1325: ⚠️ Potential issue | 🟡 Minor

Extract the repeated cloud-status shape into a shared schema.

This object graph is copied into cloud-status, cloud-refresh, cloud-profile/connect, and each profile mutation response. The drift is already visible in the selected-profile cloudUrl/linkUrl, which are weaker than the same fields inside profiles[]. Back these endpoints from one shared Zod schema/components ref so the generated spec and SDK stay aligned.

Based on learnings: All request bodies, path params, query params, and responses must have Zod schemas; shared schemas go in packages/shared/src/schemas/, route-local param schemas can stay in the route file

Also applies to: 1448-1506, 1583-1641, 1750-1808, 1926-1984, 2079-2137, 2259-2317, 2412-2470, 2586-2644

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/controller/openapi.json` around lines 1267 - 1325, Extract the repeated
"cloud-status" object into a single shared Zod/OpenAPI component and replace the
duplicated inline definitions used by cloud-status, cloud-refresh,
cloud-profile/connect and the profile mutation responses; create a Zod schema
(e.g., CloudProfileSchema or CloudStatusSchema) under
packages/shared/src/schemas/, ensure it includes name, cloudUrl (format uri),
linkUrl (format uri, nullable where appropriate), connected, polling,
userName/userEmail/connectedAt (nullable) and modelCount (integer >=0), then
update the route schemas and response definitions (the profiles array,
activeProfileName response, and any request/response bodies) to reference this
shared schema so all request bodies, path/query params and responses use the
centralized schema/component and remove the weaker/duplicated inline variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/controller/openapi.json`:
- Around line 1267-1325: Extract the repeated "cloud-status" object into a
single shared Zod/OpenAPI component and replace the duplicated inline
definitions used by cloud-status, cloud-refresh, cloud-profile/connect and the
profile mutation responses; create a Zod schema (e.g., CloudProfileSchema or
CloudStatusSchema) under packages/shared/src/schemas/, ensure it includes name,
cloudUrl (format uri), linkUrl (format uri, nullable where appropriate),
connected, polling, userName/userEmail/connectedAt (nullable) and modelCount
(integer >=0), then update the route schemas and response definitions (the
profiles array, activeProfileName response, and any request/response bodies) to
reference this shared schema so all request bodies, path/query params and
responses use the centralized schema/component and remove the weaker/duplicated
inline variants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2493b8f5-4f03-47e2-9e5d-d41f847f0d9b

📥 Commits

Reviewing files that changed from the base of the PR and between 43febd7 and 5cfbbf8.

📒 Files selected for processing (8)
  • apps/controller/openapi.json
  • apps/controller/src/routes/desktop-compat-routes.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/controller/tests/openclaw-sync.test.ts
  • apps/web/lib/api/sdk.gen.ts
  • apps/web/lib/api/types.gen.ts
  • apps/web/src/layouts/auth-layout.tsx
  • packages/shared/src/schemas/provider.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/src/layouts/auth-layout.tsx
  • apps/controller/tests/openclaw-sync.test.ts
  • packages/shared/src/schemas/provider.ts
  • apps/controller/src/routes/desktop-compat-routes.ts
  • apps/web/lib/api/sdk.gen.ts
  • apps/controller/src/store/nexu-config-store.ts
  • apps/web/lib/api/types.gen.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants