feat: redesign onboarding, boot, and LLM settings end-to-end#1885
Conversation
…ound
- Replace dark BootCheckGate panel with light theme matching app shell
(white card, primary-500 accents, stone palette, red-600 destructive).
- Extract shared mesh-gradient + dotted-canvas into AppBackground
component, consumed by both App.tsx and the BootCheckGate panel so the
boot screen inherits the same subtle background as the rest of the app.
- Rewrite picker copy in friendlier, human tone; title-case all titles
("Select a Runtime", "Run Locally (Recommended)", "Run on the Cloud
(Complex)" with 24x7 hint); rename user-facing "core" -> "runtime".
- Welcome page: drop Advanced RPC URL inline form, replace with full-width
secondary "Select a Runtime" button that resets coreMode and returns to
the picker. Update tagline to the README's "personal AI super
intelligence" line.
- Add language selector (English / 中文 with flags) to the picker panel,
using a custom chevron + appearance-none to avoid native arrow overlap.
- Picker option cards: border-2 + !important borders to keep the strong
blue selected border visible past the global *:focus reset.
- Fade .app-dotted-canvas dots from 0.5 to 0.3 alpha.
- test-utils + BootCheckGate test store now register the locale reducer;
all assertions updated to new copy.
New flow: welcome -> runtime-choice -> (api-keys if Custom) -> /home.
The Composio/Gmail skills step and its dependent context-gathering step
are removed from the flow (files kept on disk) because requiring a
Gmail connect on first launch was scaring users off.
- RuntimeChoiceStep: two side-by-side cards.
* Simple (sage tint, "Recommended"): "Let OpenHuman manage everything
for you" + bullets, with a highlighted "$1 free credit" callout.
* Custom (primary tint): bring-your-own-keys path; no Ollama copy.
- ApiKeysStep: optional OpenAI + Anthropic key inputs shown only on the
Custom path, persisted via setCloudProviderKey. Skip link bypasses.
- OnboardingDraft gains aiMode: 'cloud' | 'custom'.
- BetaBanner copy rewritten to a single friendly beta line with a
"Send feedback" Discord link; no longer renders welcomeDesc.
- OnboardingLayout widened to max-w-2xl so the side-by-side cards fit.
- Strip the global *:focus border/box-shadow !important overrides from
index.css so colored selected borders (BootCheckGate picker, runtime
picker) actually stay visible when the element is focused. Only the
default browser focus outline + tap highlight are suppressed now.
…domain
Picking Custom on the runtime-choice screen now walks the user through
five domain-specific steps:
runtime-choice → custom/inference → voice → oauth → search → memory → /home
Each step has the same shape:
- Step counter (N of 5) + title + subtitle
- Two side-by-side cards: Default (let OpenHuman manage it) vs
Configure (I'll pick what to use)
- Inline content slot rendered when Configure is selected
- Back / Continue footer
Inference Configure renders OpenAI + Anthropic key inputs inline and
saves via setCloudProviderKey before advancing (empty form = advance
without saving). The other four steps render a ConfigureLaterCallout
that records the choice and surfaces the matching /settings/... deep
link; they're stubs pending per-panel "minimal" prop work.
Also:
- OnboardingDraft gains customChoices: per-domain default/configure
marker so Back navigation restores the user's selection.
- Restored a "Can be free if you run everything locally" bullet on the
Custom runtime-choice card (no Ollama mention).
- Routes + nav wired through customWizardSteps.ts so adding/reordering
steps is a one-line edit.
ApiKeysStep/Page from the previous commit are now orphaned (functionality
folded into CustomInferencePage); kept on disk for now.
…s cleanup Onboarding custom wizard: - New WizardStepper renders a horizontal dot/check indicator at the top of every CustomWizardStep (upcoming = stone outline, active = primary blue, completed = sage with check + sage connector line). Replaces the plain "Step N of M" text label. - Each of the five settings panels (AIPanel, VoicePanel, ComposioPanel, ToolsPanel, MemoryDataPanel) now accepts an `embedded?: boolean` prop. When true, the panel skips its SettingsHeader chrome and drops the outer p-4 padding so it sits inside another flow. All logic + state + save UI stay intact. - The wizard's Inference/Voice/OAuth pages now embed the real settings panels inline (AIPanel/VoicePanel/ComposioPanel) instead of showing a placeholder + deep-link callout. - Search and Memory steps are commented out of CUSTOM_WIZARD_STEPS and the route table for now (page files kept on disk). Each remaining step auto-detects whether it's last (STEP_INDEX === length-1) and either navigates forward or completeAndExit's with "Finish Setup". Settings: - Extracted a shared LanguageSelect component (used by BootCheckGate and SettingsHome). Renders flag + label options with the custom chevron + appearance-none styling, dispatching setLocale on change. - SettingsHome is now a flat list. Removed group headers (General / Features & AI / Billing & Rewards / Support / Advanced / Danger Zone), the Rewards menu item, and the Restart Tour + About items. Existing tests rewritten to match. - Restart Tour + About moved into DeveloperOptionsPanel as trailing items below the existing dev navigation. DeveloperOptionsPanel test mock extended with CORE_RPC_URL + a resetWalkthrough mock to cover the new transitive imports. Copy: - ComposioPanel intro + radio labels rewritten: "Managed (OpenHuman handles it for you)" with "OpenHuman manages the Composio connection for you" copy, replacing the "TinyHumans backend" wording. - nav.memory renamed "Intelligence" (bottom tab now reads Intelligence). - Dropped the misc.beta chip from the Dreams tab on the Intelligence page (the long beta sentence didn't belong in a tiny pill).
- DeveloperOptionsPanel had an "AI Configuration" row that pointed at /settings/ai — the same target as the top-level AI tile in SettingsHome. Removed the duplicate to keep one entry point. - Renamed the top-level tile from "AI" to "AI Configuration" so the label matches what the panel actually configures. Test assertion updated to match.
LLM panel (AIPanel): - Reorganised into two top-level sections with explicit headers: * Auth — cloud providers list + local Ollama runtime setup * Routing — per-workload model assignment - Replaced the 3-way Primary / Cloud / Local segmented control on each workload row with a binary Default / Custom toggle. Default uses the primary provider (OpenHuman by default); Custom opens a popup that lets the user pick a specific provider and model. - Added CustomRoutingDialog modal: provider dropdown (custom cloud providers + Ollama if running) plus a model input (free-text for cloud, dropdown of installed models for local). Saves through the same ProviderRef shape so existing storage is unchanged. - Dropped the Cloud / Local / Mixed preset pills — the new per-row binary toggle covers that affordance without ambiguity. Settings home / Advanced: - Removed the Alerts bottom tab; added an "Alerts" Settings menu item that navigates to /notifications (the inbox). - Removed the Features tile entirely; moved Screen Awareness, Messaging Channels, Tools, and AI Configuration into Advanced (DeveloperOptionsPanel). - Renamed the "Developer Options" tile + panel header to "Advanced" (settings.developerOptions + devOptions.title). - Moved the CoreModeBadge / LogsFolderRow / SentryTestRow diagnostics callouts below the menu list, with consistent gap-3 spacing. - Fixed the invisible "Local" pill on CoreModeBadge: it was using the non-existent `ocean-*` tailwind palette; swapped to `primary-*`. - SettingsHome test rewritten to match the trimmed-down menu.
…de Zoom/Meet LLM panel: - Renamed the top section from "Auth" to "LLM Providers" and rewrote the blurb to describe the chip toggles directly. - Replaced the CloudProviderCard list with a row of compact ProviderToggleChip pills, one per supported backend (OpenAI, Anthropic, OpenRouter, LM Studio, Ollama, Custom). OpenHuman is hidden — it's always on by the signed-in session. - Each chip wears its faint brand tint at ALL times (enabled and disabled); only the inline toggle switch signals connected state. Tints: OpenAI emerald, Anthropic orange, OpenRouter slate, LM Studio cyan, Ollama violet, Custom stone. - Toggling ON opens a minimal ProviderKeyDialog (single password input). Toggling OFF removes the provider and scrubs any pinned routing entries that referenced it. - LM Studio + Ollama are persisted as `type: 'custom'` with their label as the discriminator, so the wire format stays unchanged. A small pendingLocalLabel state threads the label through the dialog's save. - Removed the entire "Local provider" Ollama runtime section + its helpers (TIER_PRESETS, formatBytes, StatusDot, ProviderChip, CloudProviderCard, advancedOpen state, custom-path input, daemon- conflict derived flag) and the now-unused imports. Routing (unchanged in this commit, recap for context): - Workload rows are Default vs Custom; Custom opens a popup to pick a provider + model. Webview accounts: - Hid Google Meet and Zoom from the BASE_PROVIDERS list in types/accounts.ts. Their AccountProvider ids stay in the type union so existing accounts keep rendering. Tests: - AIPanel.test.tsx now asserts the "LLM Providers" header instead of the old "Auth" / "Cloud providers" labels and confirms the Local provider sub-section is no longer present.
When a user toggles a provider on, the new ProviderKeyDialog now hits the provider's "list models" endpoint with the pasted key before persisting anything: - 401/403 → "Key rejected — check it and try again." and stays open. - Other non-2xx → surface "Models endpoint returned <status> …". - Network failure → surface the error verbatim. - On success → green "Key looks good — N models available." then save. OpenAI / Anthropic / OpenRouter have known endpoints; custom + local runtimes (LM Studio / Ollama via the chip-toggle layer) skip validation and accept the key as-is. Side benefit: the validation response is the only path where the renderer has the plaintext key in hand (the persisted copy is encrypted in the core), so it doubles as the source of truth for the custom-routing model dropdown. - `validateCloudProviderKey` now returns `modelIds` alongside the count. - New `cacheProviderModelIds` / `loadProviderModelIds` / `clearProviderModelIds` helpers persist the ID list in localStorage keyed per provider type (`openhuman.provider_model_ids.v1.<type>`). - ProviderKeyDialog forwards the IDs to AIPanel via `onSubmit`, which caches them after a successful save. - Toggling a provider off clears the cache entry. - CustomRoutingDialog now accepts a `cloudModelIds` map. When a cached list is present for the selected provider, the Model field is a dropdown of those IDs (with the currently-selected id prepended if it's missing, so user picks are never silently swallowed). When the cache is empty (custom / LM Studio / Ollama) the dialog falls back to the original free-text model input. Stick with `type="text"` + autoComplete/data-* hints on the API-key input so Chromium's password manager doesn't prompt to save the key.
Combined the split `import type` / value-import lines in the five
Custom*Page wizard pages into one `import { type Foo, foo } from ...`
declaration to satisfy `no-duplicate-imports` (the pre-push lint hook
was failing on these).
Other modified files in this commit are auto-formatting touch-ups the
linter applied on the same run (whitespace, sort order).
📝 WalkthroughWalkthroughThis PR adds an AppBackground component, restyles BootCheckGate and integrates a LanguageSelect, flattens Settings home and embeds panels for onboarding, refactors AIPanel to chip-based provider toggles with provider-key validation and model-id caching, implements a runtime-choice onboarding flow plus custom wizard steps, and updates i18n and tests. ChangesOnboarding and Settings Restructuring
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/components/settings/panels/DeveloperOptionsPanel.tsx (1)
188-189:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix provider branding typo in menu copy.
Line 188 and Line 189 use “ComposeIO”; the rest of the codebase/UI uses “Composio”.
✏️ Proposed fix
- title: 'ComposeIO Triggers', - description: 'View ComposeIO trigger history and archive', + title: 'Composio Triggers', + description: 'View Composio trigger history and archive',🤖 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 `@app/src/components/settings/panels/DeveloperOptionsPanel.tsx` around lines 188 - 189, In DeveloperOptionsPanel (the settings panel that defines the menu item with keys title and description), replace the incorrect provider branding "ComposeIO" with the correct "Composio" in both the title and description strings so the UI copy matches the rest of the codebase; update the two string values referenced in the block where title: 'ComposeIO Triggers' and description: 'View ComposeIO trigger history and archive' to use "Composio" instead.
🧹 Nitpick comments (2)
app/src/components/settings/panels/__tests__/VoicePanel.test.tsx (1)
565-566: ⚡ Quick winRemove leftover debug logging from the test.
Line 566 prints large DOM output on every run; this is noisy and non-essential for assertions.
🧹 Proposed fix
- // DEBUG: full DOM if section appears empty - - console.log('SECTION HTML:', section.outerHTML.slice(0, 2000));🤖 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 `@app/src/components/settings/panels/__tests__/VoicePanel.test.tsx` around lines 565 - 566, Remove the leftover debug logging line console.log('SECTION HTML:', section.outerHTML.slice(0, 2000)) from the VoicePanel.test.tsx test (or replace it with a debug-only conditional if you need it temporarily); locate the occurrence that prints section.outerHTML and delete that statement so the test no longer emits large DOM output during runs.app/src/services/api/aiSettingsApi.ts (1)
332-339: 💤 Low valueConsider adding a timeout to prevent indefinite hangs.
The
fetch()call has no timeout. On slow or unresponsive networks, validation could hang indefinitely, leaving the user waiting with no feedback.⏱️ Optional: Add AbortController timeout
export async function validateCloudProviderKey( providerType: CloudProviderType, apiKey: string ): Promise<ProviderValidationResult> { const cfg = PROVIDER_VALIDATION[providerType]; if (!cfg) { return { ok: true }; } try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10_000); - const res = await fetch(cfg.url, { method: 'GET', headers: cfg.authHeader(apiKey) }); + const res = await fetch(cfg.url, { + method: 'GET', + headers: cfg.authHeader(apiKey), + signal: controller.signal, + }); + clearTimeout(timeoutId); if (!res.ok) {🤖 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 `@app/src/services/api/aiSettingsApi.ts` around lines 332 - 339, Add a timeout to the fetch call used for validating API keys by using an AbortController: create an AbortController, start a setTimeout that calls controller.abort() after a sensible timeout (e.g. 5–10s), pass controller.signal into the fetch call (the fetch(...) invocation that currently uses cfg.url and cfg.authHeader(apiKey)), and clear the timeout when fetch completes; also catch the abort/DOMException and return a clear timeout error (e.g. 'Request timed out') instead of letting the validation hang. Ensure you reference the existing fetch invocation and cfg.authHeader(apiKey) so the signal is added to that call and the timeout cleared in the surrounding try/catch.
🤖 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 `@app/src/components/BootCheckGate/BootCheckGate.tsx`:
- Around line 68-72: BootCheckLanguageSelect currently hardcodes
ariaLabel="Language" which prevents localization; update the component to obtain
the translated label (e.g., via the i18n hook/translator used in the app such as
t('settings.language')) and pass that string into the LanguageSelect ariaLabel
prop so the screen-reader text follows the selected locale (look for
BootCheckLanguageSelect and the LanguageSelect ariaLabel prop to modify).
In `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 971-976: The code in the useMemo computing cloudModelIdsMap calls
loadProviderModelIds(...) (reading localStorage) which bypasses Redux; move this
provider model-id cache into Redux instead. Replace direct calls to
loadProviderModelIds inside the cloudModelIdsMap useMemo (and avoid localStorage
reads in this component) by wiring a Redux slice/state (with actions/selectors)
that stores model IDs per provider type, populate that slice on settings
initialization or when draft.cloudProviders changes, and then read the model IDs
via the new selector in cloudModelIdsMap (retain the same keys like
draft.cloudProviders and symbol names cloudModelIdsMap and loadProviderModelIds
only as references for where to change).
- Around line 630-635: The displayed provider label uses
PROVIDER_META[...].label which collapses different instances of the same
provider type (e.g., LM Studio/Ollama as 'custom'); change the logic in the
resolved computation (the branches referencing primary.type,
primary.defaultModel, selectedCloud.type, ref_.model) to prefer the provider
instance's own label/name (e.g., primary.label or selectedCloud.label or
ref_.label) when present, falling back to PROVIDER_META[type].label plus model
when instance label is missing; update the expressions that set resolved (the
blocks using primary, selectedCloud, and ref_) to use the instance label first
to avoid collisions while preserving the existing fallback behavior.
- Around line 1321-1332: The new provider object `upserted` always uses
`defaultEndpointFor(type)` so any user-entered runtime endpoint from the dialog
is lost; change the `endpoint` assignment in the `upserted` object to use the
dialog's local endpoint variable (e.g., `localEndpoint` or whatever prop/state
holds the user input) with a fallback to `defaultEndpointFor(type)`, i.e.
`endpoint: localEndpoint ?? defaultEndpointFor(type)`, then keep the existing
`setDraft({...})` and `setCloudProviderKey(type as ApiCloudProviderType,
apiKey)` calls unchanged so the entered endpoint is persisted in the draft
provider record.
In `@app/src/components/settings/SettingsHome.tsx`:
- Around line 80-95: The alerts menu item in the settings items array (the
object with id: 'alerts' in SettingsHome.tsx) uses a hardcoded description
string; replace that literal with a call to the i18n helper t(...) so it
localizes like the other entries (e.g., change description: 'View recent alerts
and activity in your inbox' to description: t('settings.alerts.description') or
the existing appropriate key), and add the new key/value to the translation
files if it doesn't exist.
In `@app/src/lib/i18n/zh-CN.ts`:
- Around line 135-137: The zh-CN locale is missing the ~97 new onboarding keys
introduced in en.ts (see key groups onboarding.runtimeChoice.*,
onboarding.apiKeys.*, onboarding.custom.*); update app/src/lib/i18n/zh-CN.ts by
adding all corresponding keys from en.ts (copy the exact key names and provide
Chinese translations or temporary placeholders with a clear TODO comment),
ensuring the exported translation object includes every
onboarding.runtimeChoice, onboarding.apiKeys, and onboarding.custom key so the
new onboarding flow won’t display English/raw keys; alternatively, if
translations can’t be provided now, disable zh-CN for the onboarding routes or
open a tracking issue and add explicit fallbacks for those keys in zh-CN.ts.
In `@app/src/pages/onboarding/components/WizardStepper.tsx`:
- Line 18: The ARIA label on the stepper is hardcoded in WizardStepper; replace
the static string aria-label="Onboarding progress" with a localized string
obtained from your i18n system (e.g., call useTranslation()/t or equivalent) so
the label follows the user's locale; update the WizardStepper component to
import and use the translation function and pass t('onboarding.progress') (or
the appropriate key) into the aria-label prop so screen readers receive the
translated text.
In `@app/src/pages/onboarding/steps/ApiKeysStep.tsx`:
- Around line 27-41: The saving state is only reset in the catch block so the
component can remain stuck loading on success; update the try/catch to include a
finally that calls setSaving(false) so setSaving is always reset regardless of
success or error, keeping the existing calls to setCloudProviderKey('openai',
...), setCloudProviderKey('anthropic', ...) and onNext() intact and only moving
the setSaving(false) into a finally after those operations and error handling.
In `@app/src/pages/onboarding/steps/CustomWizardStep.tsx`:
- Around line 49-58: The onContinue prop is invoked directly allowing
double-submits; update the CustomWizardStep component to guard against
re-entrancy by tracking an internal in-flight state (e.g., useState
isSubmitting) and wrap the existing onContinue call so clicks are ignored while
isSubmitting is true; set isSubmitting true before awaiting the async onContinue
and false after completion/error, and tie this state into the existing
continueDisabled/continueLoading/continueLoadingLabel UI so the button becomes
disabled and shows a loading label while the handler runs (refer to the
onContinue prop and continueDisabled/continueLoading/continueLoadingLabel
identifiers when making the change).
---
Outside diff comments:
In `@app/src/components/settings/panels/DeveloperOptionsPanel.tsx`:
- Around line 188-189: In DeveloperOptionsPanel (the settings panel that defines
the menu item with keys title and description), replace the incorrect provider
branding "ComposeIO" with the correct "Composio" in both the title and
description strings so the UI copy matches the rest of the codebase; update the
two string values referenced in the block where title: 'ComposeIO Triggers' and
description: 'View ComposeIO trigger history and archive' to use "Composio"
instead.
---
Nitpick comments:
In `@app/src/components/settings/panels/__tests__/VoicePanel.test.tsx`:
- Around line 565-566: Remove the leftover debug logging line
console.log('SECTION HTML:', section.outerHTML.slice(0, 2000)) from the
VoicePanel.test.tsx test (or replace it with a debug-only conditional if you
need it temporarily); locate the occurrence that prints section.outerHTML and
delete that statement so the test no longer emits large DOM output during runs.
In `@app/src/services/api/aiSettingsApi.ts`:
- Around line 332-339: Add a timeout to the fetch call used for validating API
keys by using an AbortController: create an AbortController, start a setTimeout
that calls controller.abort() after a sensible timeout (e.g. 5–10s), pass
controller.signal into the fetch call (the fetch(...) invocation that currently
uses cfg.url and cfg.authHeader(apiKey)), and clear the timeout when fetch
completes; also catch the abort/DOMException and return a clear timeout error
(e.g. 'Request timed out') instead of letting the validation hang. Ensure you
reference the existing fetch invocation and cfg.authHeader(apiKey) so the signal
is added to that call and the timeout cleared in the surrounding try/catch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 620c1d1b-0fe2-424e-8182-47ab3651f2e3
📒 Files selected for processing (48)
app/src/App.tsxapp/src/components/AppBackground.tsxapp/src/components/BootCheckGate/BootCheckGate.tsxapp/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsxapp/src/components/BottomTabBar.tsxapp/src/components/LanguageSelect.tsxapp/src/components/settings/SettingsHome.tsxapp/src/components/settings/__tests__/SettingsHome.test.tsxapp/src/components/settings/panels/AIPanel.tsxapp/src/components/settings/panels/ComposioPanel.tsxapp/src/components/settings/panels/DeveloperOptionsPanel.tsxapp/src/components/settings/panels/MemoryDataPanel.tsxapp/src/components/settings/panels/ToolsPanel.tsxapp/src/components/settings/panels/VoicePanel.tsxapp/src/components/settings/panels/__tests__/AIPanel.test.tsxapp/src/components/settings/panels/__tests__/ComposioPanel.test.tsxapp/src/components/settings/panels/__tests__/DeveloperOptionsPanel.test.tsxapp/src/components/settings/panels/__tests__/RecoveryPhrasePanel.test.tsxapp/src/components/settings/panels/__tests__/VoicePanel.test.tsxapp/src/index.cssapp/src/lib/i18n/I18nContext.tsxapp/src/lib/i18n/en.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Intelligence.tsxapp/src/pages/Welcome.tsxapp/src/pages/__tests__/Home.test.tsxapp/src/pages/__tests__/Welcome.test.tsxapp/src/pages/onboarding/Onboarding.tsxapp/src/pages/onboarding/OnboardingContext.tsxapp/src/pages/onboarding/OnboardingLayout.tsxapp/src/pages/onboarding/components/BetaBanner.tsxapp/src/pages/onboarding/components/ConfigureLaterCallout.tsxapp/src/pages/onboarding/components/WizardStepper.tsxapp/src/pages/onboarding/customWizardSteps.tsapp/src/pages/onboarding/pages/ApiKeysPage.tsxapp/src/pages/onboarding/pages/CustomInferencePage.tsxapp/src/pages/onboarding/pages/CustomMemoryPage.tsxapp/src/pages/onboarding/pages/CustomOAuthPage.tsxapp/src/pages/onboarding/pages/CustomSearchPage.tsxapp/src/pages/onboarding/pages/CustomVoicePage.tsxapp/src/pages/onboarding/pages/RuntimeChoicePage.tsxapp/src/pages/onboarding/pages/WelcomePage.tsxapp/src/pages/onboarding/steps/ApiKeysStep.tsxapp/src/pages/onboarding/steps/CustomWizardStep.tsxapp/src/pages/onboarding/steps/RuntimeChoiceStep.tsxapp/src/services/api/aiSettingsApi.tsapp/src/test/test-utils.tsxapp/src/types/accounts.ts
💤 Files with no reviewable changes (2)
- app/src/components/settings/panels/tests/RecoveryPhrasePanel.test.tsx
- app/src/pages/tests/Home.test.tsx
…i#1885 CodeRabbit quick-wins applied: - Localize the boot-screen + wizard-stepper ARIA labels via t(). New i18n keys: onboarding.custom.progressAriaLabel + reuse of settings.language for the boot-screen language select. - SettingsHome "Alerts" row: hardcoded description → new settings.alertsDesc i18n key. - ApiKeysStep: move setSaving(false) from the catch branch to a finally so the loading state can't stick if the request resolves cleanly. - CustomWizardStep: add an internal isContinuing guard so async onContinue handlers can't be double-fired by repeated button clicks (completeAndExit + navigate are not idempotent). Widens the prop signature to `() => void | Promise<void>`. - AIPanel chip ↔ routing label collision: PROVIDER_META[type].label collapsed LM Studio and Ollama (both stored as type='custom') into a single "Custom" entry in the routing dropdown and resolved-state hints. Switched all four call sites to use `provider.label` so the instance name shows. - AIPanel local-runtime endpoint persistence: for LM Studio / Ollama the API-key dialog's input is the endpoint URL, not a secret token. Persist it as the provider endpoint and skip setCloudProviderKey (no remote credential to store). Two CodeRabbit suggestions are intentionally deferred and replied to inline: - "Move provider model-id cache out of localStorage into Redux" — scoped out for this PR. The renderer doesn't have access to the plaintext key after save (encrypted in core), so caching at validation time is what makes the Custom-routing dropdown work at all. A Redux migration is a follow-up. - "Incomplete zh-CN translations for new onboarding" — tracked as translation debt. The English strings render as fallback today; a follow-up will add the ~97 new keys.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx`:
- Around line 1103-1105: The chip selection is matching custom providers by
mutable cp.label so state breaks after rehydrate; change the equality check in
the draft.cloudProviders.find calls (the one using cp.type === 'custom' &&
cp.label === label and the similar block at 1325-1329) to match on a durable
identifier that is persisted (e.g., cp.id, cp.providerId or cp.kind) instead of
cp.label; locate the places in AIPanel.tsx that currently compare cp.label and
update them to compare the stable property (and ensure label is only used for
display while the durable id is used for identity).
- Around line 1333-1336: The code adds the new provider to draft.cloudProviders
before awaiting setCloudProviderKey, so the UI shows the provider enabled even
if key persistence fails; change the flow in the block where setDraft, upserted,
isLocalRuntime, type and apiKey are used so you first call and await
setCloudProviderKey(type as ApiCloudProviderType, apiKey) (skipping this call
only for isLocalRuntime or when type === 'openhuman'), and only after that
succeeds update state via setDraft({ ...draft, cloudProviders:
[...draft.cloudProviders, upserted] }); alternatively, if you must
optimistically add it, ensure you roll back the draft change on any caught error
from setCloudProviderKey and show an error to the user.
- Around line 546-549: The provider key input in AIPanel.tsx (the <input
id="provider-key-input"> element) is currently plaintext; change it to use
type="password" to mask cloud API keys by default and only render as a URL/text
input when the selected provider is in an endpoint/LM Studio/Ollama mode; update
the render logic in the AIPanel component to choose input type based on the
provider mode (e.g., `type={isEndpointMode ? "url" /* or "text" if needed */ :
"password"}`) while preserving autoComplete="off" and any existing id/props so
the rest of the component works unchanged.
- Around line 509-514: The call to validateCloudProviderKey in handleSave can
throw and leave the UI stuck in the 'testing' phase; wrap the await
validateCloudProviderKey(type, trimmed) in a try/catch inside handleSave (or the
function that calls it), and on catch call setError with a user-friendly message
(and optionally log the error) and setPhase('idle') before returning so the
dialog never remains stuck in "Testing…". Ensure existing success/error handling
(using result.ok, setError, setPhase) remains unchanged for the non-throwing
path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 69bec93e-e80b-4463-8c1d-d724154234ac
📒 Files selected for processing (7)
app/src/components/BootCheckGate/BootCheckGate.tsxapp/src/components/settings/SettingsHome.tsxapp/src/components/settings/panels/AIPanel.tsxapp/src/lib/i18n/en.tsapp/src/pages/onboarding/components/WizardStepper.tsxapp/src/pages/onboarding/steps/ApiKeysStep.tsxapp/src/pages/onboarding/steps/CustomWizardStep.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/lib/i18n/en.ts
- app/src/pages/onboarding/steps/ApiKeysStep.tsx
- app/src/pages/onboarding/components/WizardStepper.tsx
| const result = await validateCloudProviderKey(type, trimmed); | ||
| if (!result.ok) { | ||
| setError(result.error ?? "Couldn't verify that key. Please try again."); | ||
| setPhase('idle'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Handle validator throw paths to avoid a stuck “Testing…” state.
validateCloudProviderKey(...) is awaited without a surrounding try/catch. If it throws (network/runtime), handleSave rejects and leaves the dialog in testing.
Suggested fix
- setPhase('testing');
- const result = await validateCloudProviderKey(type, trimmed);
+ setPhase('testing');
+ let result;
+ try {
+ result = await validateCloudProviderKey(type, trimmed);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Couldn't verify that key. Please try again.");
+ setPhase('idle');
+ return;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const result = await validateCloudProviderKey(type, trimmed); | |
| if (!result.ok) { | |
| setError(result.error ?? "Couldn't verify that key. Please try again."); | |
| setPhase('idle'); | |
| return; | |
| } | |
| let result; | |
| try { | |
| result = await validateCloudProviderKey(type, trimmed); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : "Couldn't verify that key. Please try again."); | |
| setPhase('idle'); | |
| return; | |
| } | |
| if (!result.ok) { | |
| setError(result.error ?? "Couldn't verify that key. Please try again."); | |
| setPhase('idle'); | |
| return; | |
| } |
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 509 - 514, The
call to validateCloudProviderKey in handleSave can throw and leave the UI stuck
in the 'testing' phase; wrap the await validateCloudProviderKey(type, trimmed)
in a try/catch inside handleSave (or the function that calls it), and on catch
call setError with a user-friendly message (and optionally log the error) and
setPhase('idle') before returning so the dialog never remains stuck in
"Testing…". Ensure existing success/error handling (using result.ok, setError,
setPhase) remains unchanged for the non-throwing path.
| <input | ||
| id="provider-key-input" | ||
| type="text" | ||
| autoComplete="off" |
There was a problem hiding this comment.
Mask cloud API key entry instead of showing plaintext.
The key input is type="text", which exposes secrets on screen. Use password masking for cloud providers; keep URL-style input only for LM Studio/Ollama endpoint mode.
Suggested fix
+ const isLocalEndpointInput = type === 'custom' && (label === 'LM Studio' || label === 'Ollama');
@@
- <input
+ <input
id="provider-key-input"
- type="text"
+ type={isLocalEndpointInput ? 'url' : 'password'}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <input | |
| id="provider-key-input" | |
| type="text" | |
| autoComplete="off" | |
| const isLocalEndpointInput = type === 'custom' && (label === 'LM Studio' || label === 'Ollama'); | |
| <input | |
| id="provider-key-input" | |
| type={isLocalEndpointInput ? 'url' : 'password'} | |
| autoComplete="off" |
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 546 - 549, The
provider key input in AIPanel.tsx (the <input id="provider-key-input"> element)
is currently plaintext; change it to use type="password" to mask cloud API keys
by default and only render as a URL/text input when the selected provider is in
an endpoint/LM Studio/Ollama mode; update the render logic in the AIPanel
component to choose input type based on the provider mode (e.g.,
`type={isEndpointMode ? "url" /* or "text" if needed */ : "password"}`) while
preserving autoComplete="off" and any existing id/props so the rest of the
component works unchanged.
| const existing = draft.cloudProviders.find( | ||
| cp => cp.type === 'custom' && cp.label === label | ||
| ); |
There was a problem hiding this comment.
LM Studio/Ollama enable-state identity is not durable across reload.
Chip state depends on cp.label matching "LM Studio"/"Ollama", but save/load adapters do not persist label and rehydrate custom providers with generic metadata labels. After reload, chips can appear disconnected and allow duplicate inserts.
Also applies to: 1325-1329
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 1103 - 1105, The
chip selection is matching custom providers by mutable cp.label so state breaks
after rehydrate; change the equality check in the draft.cloudProviders.find
calls (the one using cp.type === 'custom' && cp.label === label and the similar
block at 1325-1329) to match on a durable identifier that is persisted (e.g.,
cp.id, cp.providerId or cp.kind) instead of cp.label; locate the places in
AIPanel.tsx that currently compare cp.label and update them to compare the
stable property (and ensure label is only used for display while the durable id
is used for identity).
| setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] }); | ||
| if (!isLocalRuntime && type !== 'openhuman') { | ||
| await setCloudProviderKey(type as ApiCloudProviderType, apiKey); | ||
| } |
There was a problem hiding this comment.
Do not flip provider ON before key persistence succeeds.
The provider is added to draft.cloudProviders before setCloudProviderKey(...) completes. If key persistence fails, UI still shows provider enabled with no stored credential.
Suggested fix
- setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] });
- if (!isLocalRuntime && type !== 'openhuman') {
- await setCloudProviderKey(type as ApiCloudProviderType, apiKey);
- }
+ if (!isLocalRuntime && type !== 'openhuman') {
+ await setCloudProviderKey(type as ApiCloudProviderType, apiKey);
+ }
+ setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] }); | |
| if (!isLocalRuntime && type !== 'openhuman') { | |
| await setCloudProviderKey(type as ApiCloudProviderType, apiKey); | |
| } | |
| if (!isLocalRuntime && type !== 'openhuman') { | |
| await setCloudProviderKey(type as ApiCloudProviderType, apiKey); | |
| } | |
| setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] }); |
🤖 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 `@app/src/components/settings/panels/AIPanel.tsx` around lines 1333 - 1336, The
code adds the new provider to draft.cloudProviders before awaiting
setCloudProviderKey, so the UI shows the provider enabled even if key
persistence fails; change the flow in the block where setDraft, upserted,
isLocalRuntime, type and apiKey are used so you first call and await
setCloudProviderKey(type as ApiCloudProviderType, apiKey) (skipping this call
only for isLocalRuntime or when type === 'openhuman'), and only after that
succeeds update state via setDraft({ ...draft, cloudProviders:
[...draft.cloudProviders, upserted] }); alternatively, if you must
optimistically add it, ensure you roll back the draft change on any caught error
from setCloudProviderKey and show an error to the user.
Summary
AppBackgroundcomponent). Friendlier copy, language selector with flags, "Select a Runtime" CTA on the Welcome screen that returns to the picker.embeddedprop./notifications(the Alerts bottom tab is gone).AIPanel): split into two top-level sections — LLM Providers (chip-toggle layout with always-on brand tints for OpenAI / Anthropic / OpenRouter / LM Studio / Ollama / Custom) and Routing (per-workload binary toggle: Default uses OpenHuman, Custom opens a popup to pick provider + model). Local provider Ollama section deleted./v1/modelsendpoint with the pasted key before persisting. The returned model IDs are cached per-type in localStorage so the Custom routing dialog can show a real model dropdown instead of a free-text input.LanguageSelectused by both boot screen and Settings.Problem
The old onboarding required users to OAuth into Gmail on first launch — too high-friction for a first impression. The boot screen, Welcome page, and LLM Settings page each used inconsistent visual languages (dark overlays, list-based provider cards, ambiguous "Primary/Cloud/Local" routing modes) that made it hard to explain what was happening to new users.
Solution
AppBackground,LanguageSelect,WizardStepper,CustomWizardStep,ProviderToggleChip,ProviderKeyDialog,CustomRoutingDialog) so the new flows share components rather than each owning their own one-off styling.embeddedprop to the 5 settings panels (AIPanel, VoicePanel, ComposioPanel, ToolsPanel, MemoryDataPanel) — when set, the panel skips its ownSettingsHeaderchrome and reduces padding so the host (the onboarding wizard) provides the frame. All internal save/state logic is intact.validateCloudProviderKeyhelper inaiSettingsApi.ts. Same path captures the model-id list and stashes it viacacheProviderModelIds. Toggling a provider off clears the cache.*:focusoverrides inindex.cssthat were silently wiping component-controlled border colors on selected cards.Submission Checklist
aiSettingsApi.tsand is fetch-based, hard to assert deterministically in unit tests). Trusting CI's diff-cover report to call out specific files.fetchis renderer-side against the providers' public model endpoints, only when the user explicitly pastes a key).Impact
bg-[#f5f5f5]+ mesh gradient consistently across platforms.AccountProviderids in the type union so any pre-existing accounts continue to render.Related
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
launch/may-154794ee277932397d6759817dfe7e9a64aaf15a57Validation Run
pnpm --filter openhuman-app format:checkpnpm typecheckpnpm debug unit(2268 passed, 3 skipped)Validation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Parity Contract
resetCoreMode+ storage-clear that BootCheckGate's "Pick a Different Runtime" already used; the Custom wizard's last step callscompleteAndExitexactly like the prior flow.Summary by CodeRabbit
New Features
UI/UX Improvements
Settings & Personalization