refactor(desktop): add cloud profile management flow#432
Conversation
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughRemoved 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Deploying nexu-docs with
|
| 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 |
There was a problem hiding this comment.
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:
- Calling
authClient.signOut()before navigation (likeinvite.tsxdoes), or- 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 | 🟠 MajorThe return-to-claim flow is incomplete and will break the claim process for unauthenticated users.
The code saves
CLAIM_RETURN_KEYto 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_KEYin 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 | 🟠 MajorAuthentication flow loses Feishu binding context.
The "Create account" and "Log in" links send unauthenticated users to
/, which loads WelcomePage. However, WelcomePage does not support areturnToquery parameter—it redirects authenticated users directly to/workspace, losing thewsandbotparams 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 ininvite.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
cloudProfilesFileSchemarequiresschemaVersionas a positive integer, but if users have existing cloud profile files without this field (or from a previous format), schema validation will fail. UnlikenexuConfigSchema(lines 105-145) which usesz.preprocess()to normalize missing fields, this schema has no migration path.If
cloud-profiles.jsonis 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 tonexuConfigSchema.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
CloudStatustype duplicates the shape defined in@nexu/shared. While the renderer may not directly import Zod schemas, you could usez.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
cloudMessagecheck 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 callingensureValidDefaultModel()for consistency.The
cloud-profile/connecthandler does not callmodelProviderService.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 sharedDesktopCloudProfileInputtype.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.
writeActiveDesktopCloudStatewrites identical data to bothdesktop.cloudanddesktop.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.cloudis the primary read location whilecloudSessionsprovides 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.modelswithout 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
📒 Files selected for processing (40)
apps/controller/.env.exampleapps/controller/openapi.jsonapps/controller/src/app/create-app.tsapps/controller/src/app/env.tsapps/controller/src/routes/auth-routes.tsapps/controller/src/routes/desktop-compat-routes.tsapps/controller/src/routes/misc-compat-routes.tsapps/controller/src/services/desktop-local-service.tsapps/controller/src/services/local-user-service.tsapps/controller/src/store/nexu-config-store.tsapps/controller/src/store/schemas.tsapps/controller/tests/nexu-config-store.test.tsapps/controller/tests/openclaw-sync.test.tsapps/controller/tests/route-compat.test.tsapps/desktop/main/desktop-bootstrap.tsapps/desktop/main/diagnostics-export.tsapps/desktop/main/index.tsapps/desktop/main/ipc.tsapps/desktop/main/runtime/manifests.tsapps/desktop/scripts/dev-env.shapps/desktop/scripts/dist-mac.mjsapps/desktop/shared/host.tsapps/desktop/shared/runtime-config.tsapps/desktop/src/components/desktop-shell.tsxapps/desktop/src/lib/host-api.tsapps/desktop/src/main.tsxapps/desktop/src/pages/cloud-profile-page.tsxapps/desktop/src/runtime-page.cssapps/web/lib/api/sdk.gen.tsapps/web/lib/api/types.gen.tsapps/web/src/app.tsxapps/web/src/layouts/auth-layout.tsxapps/web/src/pages/auth.tsxapps/web/src/pages/feishu-bind.tsxapps/web/src/pages/invite.tsxapps/web/src/pages/slack-claim.tsxpackages/shared/src/schemas/provider.tsspecs/change/20260317-desktop-crash-reporting/spec.mdspecs/designs/desktop-cloud-connection.mdspecs/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
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/controller/openapi.json (1)
1267-1325:⚠️ Potential issue | 🟡 MinorExtract 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-profilecloudUrl/linkUrl, which are weaker than the same fields insideprofiles[]. 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 fileAlso 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
📒 Files selected for processing (8)
apps/controller/openapi.jsonapps/controller/src/routes/desktop-compat-routes.tsapps/controller/src/store/nexu-config-store.tsapps/controller/tests/openclaw-sync.test.tsapps/web/lib/api/sdk.gen.tsapps/web/lib/api/types.gen.tsapps/web/src/layouts/auth-layout.tsxpackages/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
Summary
Notes
Summary by CodeRabbit
New Features
Removals