Skip to content

feat: redesign onboarding, boot, and LLM settings end-to-end#1885

Merged
senamakel merged 12 commits into
tinyhumansai:mainfrom
senamakel:launch/may-15
May 16, 2026
Merged

feat: redesign onboarding, boot, and LLM settings end-to-end#1885
senamakel merged 12 commits into
tinyhumansai:mainfrom
senamakel:launch/may-15

Conversation

@senamakel
Copy link
Copy Markdown
Member

@senamakel senamakel commented May 16, 2026

Summary

  • Boot screen + Welcome: replaced the dark Tauri-overlay boot panel with a light, app-shell-matching screen (mesh gradient + dotted canvas via a new shared AppBackground component). Friendlier copy, language selector with flags, "Select a Runtime" CTA on the Welcome screen that returns to the picker.
  • Onboarding: removed the scary Gmail/Composio gate entirely. New flow is Welcome → Runtime Choice (Simple vs Custom) → /home for the Simple path, or → Custom wizard (Inference → Voice → OAuth) with a visual stepper for the Custom path. Each step is Default-vs-Configure; Configure embeds the real Settings panel inline (AIPanel, VoicePanel, ComposioPanel) via a new embedded prop.
  • Settings home: flattened (no more section headers), removed the standalone Features / AI / Rewards / Restart Tour / About tiles. Renamed Developer Options → Advanced and rehomed AI Configuration + Screen Awareness + Messaging + Tools + Restart Tour + About under it. New Alerts entry deep-links to /notifications (the Alerts bottom tab is gone).
  • LLM panel (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.
  • Key validation + model cache: toggling a provider on now hits its /v1/models endpoint 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.
  • Misc: hide Zoom/Google Meet from the webview accounts list, rename the Memory bottom-tab label to "Intelligence", drop the "early beta" sub-chip from the Dreams tab, light-touch i18n + copy polish, shared LanguageSelect used 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

  • Built reusable chrome (AppBackground, LanguageSelect, WizardStepper, CustomWizardStep, ProviderToggleChip, ProviderKeyDialog, CustomRoutingDialog) so the new flows share components rather than each owning their own one-off styling.
  • Added an embedded prop to the 5 settings panels (AIPanel, VoicePanel, ComposioPanel, ToolsPanel, MemoryDataPanel) — when set, the panel skips its own SettingsHeader chrome and reduces padding so the host (the onboarding wizard) provides the frame. All internal save/state logic is intact.
  • Validation lives in a new validateCloudProviderKey helper in aiSettingsApi.ts. Same path captures the model-id list and stashes it via cacheProviderModelIds. Toggling a provider off clears the cache.
  • Trimmed the global *:focus overrides in index.css that were silently wiping component-controlled border colors on selected cards.

Submission Checklist

  • N/A: this PR adds React UI redesigns and a key-validation helper; unit tests for the chrome (AIPanel, BootCheckGate, SettingsHome, DeveloperOptionsPanel, onboarding pages) were updated to match the new shape and assertions, but no new "happy + failure" test pairs were added per-component because most changes are presentational. The full Vitest suite (2268 tests) is green on this branch.
  • N/A: Diff coverage — this PR is a large refactor of presentational surfaces; new branching logic is minimal (key validation lives in aiSettingsApi.ts and is fetch-based, hard to assert deterministically in unit tests). Trusting CI's diff-cover report to call out specific files.
  • N/A: coverage matrix — behaviour-only and presentational changes; no new feature rows.
  • N/A: all feature IDs listed — see above, no new IDs.
  • No new external network dependencies introduced (the key-validation fetch is renderer-side against the providers' public model endpoints, only when the user explicitly pastes a key).
  • N/A: manual smoke — release-cut surfaces unchanged.
  • N/A: linked issue — no single tracking issue.

Impact

  • Desktop: redesigns the first-launch experience and the LLM settings UI; behaviour is preserved (storage formats, RPC contracts, AISettings save/load path all unchanged).
  • Web build: same changes apply; the boot screen now uses bg-[#f5f5f5] + mesh gradient consistently across platforms.
  • No migration required — existing user provider configurations + routing entries still parse via the same wire format. Hidden Zoom/Meet entries keep their AccountProvider ids in the type union so any pre-existing accounts continue to render.

Related

  • Closes:
  • Follow-up PR(s)/TODOs: per-domain "minimal" prop on each embedded panel so the inline forms during the Custom wizard show only the essential controls (currently they show the full panel).

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: launch/may-15
  • Commit SHA: 4794ee277932397d6759817dfe7e9a64aaf15a57

Validation Run

  • pnpm --filter openhuman-app format:check
  • pnpm typecheck
  • Focused tests: pnpm debug unit (2268 passed, 3 skipped)
  • N/A: Rust fmt/check (no Rust changes in this PR)
  • N/A: Tauri fmt/check (no Tauri shell changes in this PR)

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: First-launch onboarding no longer requires a Gmail/Composio OAuth. New users hit Welcome → Runtime Choice and complete in seconds on the Simple path. The LLM Settings page changes from a list-of-providers + 3-way routing model to a chip-toggle list + binary Default/Custom routing with a model-picker dialog.
  • User-visible effect: Smaller, less-intimidating onboarding; clearer "what's enabled" signal via brand-tinted provider chips; model dropdown (populated by the validation call) instead of free-text in the Custom routing dialog.

Parity Contract

  • Legacy behavior preserved: AISettings shape, save/load contract, ProviderRef serialization, CoreMode persistence, and BootCheckGate transport are all unchanged. The orphaned onboarding step files (SkillsStep / ContextGatheringStep / ApiKeysStep) are kept on disk for potential re-enable.
  • Guard/fallback/dispatch parity checks: Welcome's "Select a runtime" button dispatches the same resetCoreMode + storage-clear that BootCheckGate's "Pick a Different Runtime" already used; the Custom wizard's last step calls completeAndExit exactly like the prior flow.

Summary by CodeRabbit

  • New Features

    • Guided runtime selection and custom onboarding wizard
    • Provider API key validation and model-id caching
    • Language selector added to settings and onboarding
    • Dreams tab enabled
  • UI/UX Improvements

    • Simplified, flat settings menu and added Alerts entry
    • Updated boot/runtime verification screens and wording
    • Refined app background and focus styling
  • Settings & Personalization

    • Redesigned AI settings with provider chips and per-workload routing
    • Several settings panels support embedded layouts

Review Change Stack

senamakel added 10 commits May 15, 2026 16:48
…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).
@senamakel senamakel requested a review from a team May 16, 2026 02:00
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

📝 Walkthrough

Walkthrough

This 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.

Changes

Onboarding and Settings Restructuring

Layer / File(s) Summary
Background & app shell
app/src/components/AppBackground.tsx, app/src/App.tsx
Add AppBackground and replace prior MeshGradient usage in the app shell.
BootCheckGate visual update & language select
app/src/components/BootCheckGate/BootCheckGate.tsx
Restyle picker/result screens to a light theme, use AppBackground, and add inline LanguageSelect.
BootCheckGate tests
app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx
Update tests for new copy, labels, validation strings, and register locale reducer in test store.
LanguageSelect & Tab changes
app/src/components/LanguageSelect.tsx, app/src/components/BottomTabBar.tsx
New LanguageSelect component; notifications tab removed from BottomTabBar (moved to Settings deep-link).
SettingsHome flattening
app/src/components/settings/SettingsHome.tsx
Flatten settings to a single list, integrate LanguageSelect, add Alerts item, remove grouped Features/AI/Rewards/Support items.
SettingsHome tests
app/src/components/settings/__tests__/SettingsHome.test.tsx
Adjust tests to assert flat menu and new navigation for Alerts and Advanced.
Panels: embedded prop
app/src/components/settings/panels/{Composio,Memory,Tools,Voice,DeveloperOptions}Panel.tsx
Add optional embedded prop to hide SettingsHeader and adjust layout for onboarding embedding.
DeveloperOptionsPanel changes
app/src/components/settings/panels/DeveloperOptionsPanel.tsx
Redefine developer items, separate diagnostics area, add trailing items (Restart Tour, About) with handlers.
AIPanel refactor
app/src/components/settings/panels/AIPanel.tsx
Switch to chip-based provider toggles, add ProviderKeyDialog and CustomRoutingDialog, support embedded prop, and wire model-id caching.
AIPanel & Composio tests
app/src/components/settings/panels/__tests__/*
Update tests for new headers, chips, and Composio branding.
AI settings API
app/src/services/api/aiSettingsApi.ts
Add validateCloudProviderKey and localStorage helpers cacheProviderModelIds, loadProviderModelIds, clearProviderModelIds.
Onboarding router & types
app/src/pages/onboarding/{Onboarding,OnboardingContext}.tsx
Add AiMode/CustomStepKey/CustomStepChoice, extend OnboardingDraft, and add custom-step routes.
Wizard UI & helpers
app/src/pages/onboarding/components/{WizardStepper,ConfigureLaterCallout}.tsx, app/src/pages/onboarding/customWizardSteps.ts
Add stepper, configure-later callout, and step/route maps; widen onboarding layout.
Onboarding steps & pages
app/src/pages/onboarding/{steps,pages}/*
Implement RuntimeChoiceStep, CustomWizardStep, ApiKeysStep, and custom wizard pages (Inference, Voice, OAuth, Search, Memory) with analytics and routing.
Welcome updates
app/src/pages/Welcome.tsx, app/src/pages/onboarding/pages/WelcomePage.tsx
Remove advanced RPC URL flow; add "Select a Runtime" action that clears runtime caches and resets core mode; update onboarding navigation target.
Tests & test-utils
app/src/pages/__tests__/*, app/src/test/test-utils.tsx
Standardize tests to use renderWithProviders, add locale reducer to test root reducer, remove stale ESLint suppressions.
i18n and CSS
app/src/lib/i18n/{en,zh-CN}.ts, app/src/index.css
Add/update translations for runtime-choice and custom-wizard flow, update boot-check copy, refine focus styles, and reduce dotted-canvas opacity.
Account providers visibility
app/src/types/accounts.ts
Hide google-meet and zoom from picker while preserving type ids for existing accounts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • graycyrus

Poem

🐰 I hopped through onboarding gates so bright,
Picked cloud or custom by soft twilight,
Chips toggled providers, keys tested true,
Panels tucked quiet when embedded for you,
A smaller dot, a gentler light — huzzah, delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main changes: redesigning the onboarding, boot, and LLM settings workflows end-to-end, which aligns directly with the primary objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Fix 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 win

Remove 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 value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 02cf2ce and 4794ee2.

📒 Files selected for processing (48)
  • app/src/App.tsx
  • app/src/components/AppBackground.tsx
  • app/src/components/BootCheckGate/BootCheckGate.tsx
  • app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx
  • app/src/components/BottomTabBar.tsx
  • app/src/components/LanguageSelect.tsx
  • app/src/components/settings/SettingsHome.tsx
  • app/src/components/settings/__tests__/SettingsHome.test.tsx
  • app/src/components/settings/panels/AIPanel.tsx
  • app/src/components/settings/panels/ComposioPanel.tsx
  • app/src/components/settings/panels/DeveloperOptionsPanel.tsx
  • app/src/components/settings/panels/MemoryDataPanel.tsx
  • app/src/components/settings/panels/ToolsPanel.tsx
  • app/src/components/settings/panels/VoicePanel.tsx
  • app/src/components/settings/panels/__tests__/AIPanel.test.tsx
  • app/src/components/settings/panels/__tests__/ComposioPanel.test.tsx
  • app/src/components/settings/panels/__tests__/DeveloperOptionsPanel.test.tsx
  • app/src/components/settings/panels/__tests__/RecoveryPhrasePanel.test.tsx
  • app/src/components/settings/panels/__tests__/VoicePanel.test.tsx
  • app/src/index.css
  • app/src/lib/i18n/I18nContext.tsx
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Intelligence.tsx
  • app/src/pages/Welcome.tsx
  • app/src/pages/__tests__/Home.test.tsx
  • app/src/pages/__tests__/Welcome.test.tsx
  • app/src/pages/onboarding/Onboarding.tsx
  • app/src/pages/onboarding/OnboardingContext.tsx
  • app/src/pages/onboarding/OnboardingLayout.tsx
  • app/src/pages/onboarding/components/BetaBanner.tsx
  • app/src/pages/onboarding/components/ConfigureLaterCallout.tsx
  • app/src/pages/onboarding/components/WizardStepper.tsx
  • app/src/pages/onboarding/customWizardSteps.ts
  • app/src/pages/onboarding/pages/ApiKeysPage.tsx
  • app/src/pages/onboarding/pages/CustomInferencePage.tsx
  • app/src/pages/onboarding/pages/CustomMemoryPage.tsx
  • app/src/pages/onboarding/pages/CustomOAuthPage.tsx
  • app/src/pages/onboarding/pages/CustomSearchPage.tsx
  • app/src/pages/onboarding/pages/CustomVoicePage.tsx
  • app/src/pages/onboarding/pages/RuntimeChoicePage.tsx
  • app/src/pages/onboarding/pages/WelcomePage.tsx
  • app/src/pages/onboarding/steps/ApiKeysStep.tsx
  • app/src/pages/onboarding/steps/CustomWizardStep.tsx
  • app/src/pages/onboarding/steps/RuntimeChoiceStep.tsx
  • app/src/services/api/aiSettingsApi.ts
  • app/src/test/test-utils.tsx
  • app/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

Comment thread app/src/components/BootCheckGate/BootCheckGate.tsx
Comment thread app/src/components/settings/panels/AIPanel.tsx Outdated
Comment thread app/src/components/settings/panels/AIPanel.tsx
Comment thread app/src/components/settings/panels/AIPanel.tsx
Comment thread app/src/components/settings/SettingsHome.tsx
Comment thread app/src/lib/i18n/zh-CN.ts
Comment thread app/src/pages/onboarding/components/WizardStepper.tsx Outdated
Comment thread app/src/pages/onboarding/steps/ApiKeysStep.tsx
Comment thread app/src/pages/onboarding/steps/CustomWizardStep.tsx
senamakel added 2 commits May 15, 2026 19:17
…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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4794ee2 and 7559d79.

📒 Files selected for processing (7)
  • app/src/components/BootCheckGate/BootCheckGate.tsx
  • app/src/components/settings/SettingsHome.tsx
  • app/src/components/settings/panels/AIPanel.tsx
  • app/src/lib/i18n/en.ts
  • app/src/pages/onboarding/components/WizardStepper.tsx
  • app/src/pages/onboarding/steps/ApiKeysStep.tsx
  • app/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

Comment on lines +509 to +514
const result = await validateCloudProviderKey(type, trimmed);
if (!result.ok) {
setError(result.error ?? "Couldn't verify that key. Please try again.");
setPhase('idle');
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +546 to +549
<input
id="provider-key-input"
type="text"
autoComplete="off"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
<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.

Comment on lines +1103 to +1105
const existing = draft.cloudProviders.find(
cp => cp.type === 'custom' && cp.label === label
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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).

Comment on lines +1333 to +1336
setDraft({ ...draft, cloudProviders: [...draft.cloudProviders, upserted] });
if (!isLocalRuntime && type !== 'openhuman') {
await setCloudProviderKey(type as ApiCloudProviderType, apiKey);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant