feat(chat): account-tier independence, dynamic catalog, redesigned header - #124
Conversation
…ader
Account-tier resolution decoupled from active chat provider
* /setup-api/ai-models/status resolves clawaiAccountTier (any clawai
profile present) separately from clawaiTier (active chat provider).
Drives ClawKeep + Remote Desktop entitlement so a Max user chatting
via OpenAI keeps paid features unlocked.
* use-clawbox-login reads account-level tier with fallback to
active-provider tier for older /status responses.
* Shelf shield colour, shelf click target, and the "Free backup"
notification all bind to clawboxLogin.loggedIn (account-level)
instead of provider-equality.
User-name field
* Removed placeholder text; 5s poll of ui_user_name preference gated
by userNameEditedRef so local typing isn't clobbered.
* Translations: deleted settings.userName.placeholder from all 10
locales (parity test forbids empty values, removal beats blanking).
* CLAWBOX.md + config/clawbox-workspace-guide.md teach the agent to
call preferences_set("ui_user_name", "<name>") when offered a name.
Disk-backed catalog cache + background warmup
* /setup-api/ai-models/catalog serves from data/catalog-cache/
<provider>.json with 6h TTL. Refreshes happen out-of-band — the
route never waits on the openclaw bin (~3min CPU on Jetson).
* Boot warmup fires off refreshes for every CATALOG_PROVIDERS entry
on first import, staggered 5s. Subsequent picker opens are instant.
* Static fallbacks updated to mirror current upstream catalogs so
the warming-state UX is decent on fresh installs.
Per-provider reasoning effort levels
* Replaced the universal 9-level dropdown with REASONING_BY_PROVIDER
table sourced from each upstream's API docs:
OpenAI / Codex Off / Low / Medium / High / X-High (Medium)
Anthropic Low / Medium / High / Max (High)
Google Off / Low / Medium / High / Adaptive (Adaptive)
DeepSeek Low / Medium / High (High)
ClawBox AI Low / Medium / High (High)
OpenRouter Off / Minimal / Low / Medium / High / X-High (Medium)
* Per-provider localStorage remembers each provider's last choice.
Curated openai / openai-codex catalogs
* ALLOWED_MODEL_RE_BY_PROVIDER filters the live openclaw catalog to
gpt-5.4 + gpt-5.5 family only. Older gens (4.1, 5.0-5.3) hidden.
* openai-codex additionally hides -pro variants (ChatGPT-account auth
rejects them with "model not supported when using Codex with a
ChatGPT account").
Redesigned chat header dropdowns
* New HeaderDropdown component replaces native <select>: compact
trigger pill that truncates with "..." when squeezed, wider
popover showing full labels + hints, coral-highlighted active
option, click-outside / Esc to close.
* Three loose rounded pills with 6px gap (no segmented bar).
* Compact provider labels on the trigger ("Codex" instead of
"OpenAI Codex"); popover still shows full names.
* ClawBox AI consolidated into one provider entry with Flash/Pro
selectable via the secondary model dropdown — matches how
Anthropic, OpenAI, etc work. chat/model/route options now keyed
by provider (one row per provider) instead of by model id.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthroughThis PR adds user name persistence, implements ClawAI as an account-level entitlement tier separate from chat provider selection, introduces a custom header dropdown UI component, refactors chat header controls with per-provider reasoning configuration, and redesigns the model catalog system with disk-backed caching and background refresh mechanics. ChangesUser Name Preferences & Settings Polling
ClawAI Account-Level Entitlement System
Header Dropdown Component & Chat UI Refactoring
Model Catalog Caching & Route Updates
Sequence Diagram(s)sequenceDiagram
participant User as User
participant ChatUI as Chat UI
participant Status as /setup-api/ai-models/status
participant Portal as ClawBox Portal
participant Config as Config Store
User->>ChatUI: Select provider (e.g., OpenAI)
ChatUI->>Status: GET /setup-api/ai-models/status
Status->>Config: Read stored clawai/deepseek models
alt clawai model configured
Status->>Portal: Fetch account tier (if claw_ token exists)
Portal-->>Status: Account tier (e.g., "pro")
Status->>Status: clawaiAccountTier = "pro"
Status->>Status: clawaiTier = null (provider is OpenAI, not clawai)
else no clawai model
Status->>Status: clawaiAccountTier = null
Status->>Status: clawaiConfigured = false
end
Status-->>ChatUI: { clawaiAccountTier, clawaiTier, clawaiConfigured }
ChatUI->>ChatUI: useClawboxLogin derives loggedIn from clawaiConfigured
sequenceDiagram
participant User as User
participant ChatPopup as ChatPopup Component
participant HeaderDropdown as HeaderDropdown
participant LS as localStorage
participant Session as Gateway /sessions/patch
User->>ChatPopup: Switch provider (e.g., DeepSeek)
ChatPopup->>ChatPopup: activeProvider changes
ChatPopup->>LS: Read persisted thinkingLevel for new provider
LS-->>ChatPopup: Provider-specific level (or default fallback)
ChatPopup->>ChatPopup: Update effectiveThinkingLevel
ChatPopup->>HeaderDropdown: Render reasoning dropdown with provider levels
User->>HeaderDropdown: Click reasoning level option
HeaderDropdown->>ChatPopup: onChange(selected level)
ChatPopup->>LS: Persist thinkingLevel for this provider
ChatPopup->>Session: PATCH /sessions with effectiveThinkingLevel
Session-->>ChatPopup: { updated session }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
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. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 25 minutes and 13 seconds.Comment |
- Extract compareCatalogModels helper to share the sort comparator between the openclaw and openrouter transforms. - Convert refreshInBackground's nested ternary to an if/else chain per CLAUDE.md (no nested ternaries). - Replace bootWarmup's index-based for loop with forEach((p, i) => ...). - Drop the now-trivial labelForThinkingLevel useCallback wrapper and inline the THINKING_LEVEL_LABELS lookup at its single call site. - Untangle the activeModelId clawai/deepseek alias resolution into a small if-block so the picker no longer relies on a nested ternary inside a ?? chain.
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/setup-api/ai-models/catalog/route.ts (1)
225-236:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep ClawBox AI non-custom in the route payload too.
buildPayload()and the empty warming response both hardcodeallowCustom: true. After the first background refresh, the liveclawaicatalog will therefore contradictPROVIDER_CATALOGS.clawai.allowCustom = falseand re-enable arbitrary model ids in any UI that trusts this route.Suggested fix
+const ALLOW_CUSTOM_BY_PROVIDER: Record<string, boolean> = { + clawai: false, + anthropic: true, + openai: true, + "openai-codex": true, + google: true, + openrouter: true, +}; function buildPayload(provider: string, models: CatalogModel[]): CatalogResponse { const fallbackDefault = DEFAULT_MODEL_BY_PROVIDER[provider]; const defaultModelId = models.find((m) => m.id === fallbackDefault)?.id ?? models[0]?.id ?? fallbackDefault ?? ""; return { provider, models, defaultModelId, - allowCustom: true, + allowCustom: ALLOW_CUSTOM_BY_PROVIDER[provider] ?? true, fetchedAt: Date.now(), }; }const empty: CatalogResponse = { provider, models: [], defaultModelId: DEFAULT_MODEL_BY_PROVIDER[provider] ?? "", - allowCustom: true, + allowCustom: ALLOW_CUSTOM_BY_PROVIDER[provider] ?? true, fetchedAt: 0, warming: true, };Also applies to: 392-399
🤖 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 `@src/app/setup-api/ai-models/catalog/route.ts` around lines 225 - 236, The payload currently forces allowCustom: true which re-enables custom model IDs for providers like "clawai"; update buildPayload(provider, models) to set allowCustom based on the provider catalog config (e.g., use PROVIDER_CATALOGS[provider]?.allowCustom or a sensible default) instead of hardcoding true, and apply the same change to the empty/warming response path so both the real and fallback responses respect PROVIDER_CATALOGS for the given provider (refer to buildPayload and the warming/empty response construction).src/app/setup-api/ai-models/status/route.ts (1)
197-204:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNormalize provider aliases before choosing the active profile.
Line 197 derives the provider hint from the raw model slug, and Line 203 compares it to the raw profile provider. If the active model is
deepseek/...but the matching profile is stored asclawai(or the reverse),activeKeymisses and this route falls back toprofileKeys[0], which can report the wrongprovider,mode, andclawaiTier.Suggested fix
- const primaryProviderHint = model ? model.split("/")[0] : null; + const primaryProviderHint = normalizeProvider(model ? model.split("/")[0] : null); let activeKey: string | undefined; if (primaryProviderHint) { activeKey = profileKeys.find((key) => { const entry = profiles[key]; - const entryProvider = entry?.provider ?? key.split(":")[0]; + const entryProvider = normalizeProvider(entry?.provider ?? key.split(":")[0]); return entryProvider === primaryProviderHint; }); }🤖 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 `@src/app/setup-api/ai-models/status/route.ts` around lines 197 - 204, The provider comparison when selecting activeKey uses raw strings (primaryProviderHint and entry?.provider) which misses aliases like "deepseek" vs "clawai"; normalize both sides before comparing: add/use a normalization function (e.g., normalizeProviderAlias) and call it on primaryProviderHint and on entryProvider (derived from entry?.provider ?? key.split(":")[0]) inside the profileKeys.find in the activeKey assignment so aliases map to the same canonical provider name while preserving the existing fallback to profileKeys[0].
🤖 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 `@Clawbox.md`:
- Line 111: The docs show preferences_set as two positional args but the MCP
exposes a single parameter named "preferences" which is a JSON string parsed
with JSON.parse; update the documentation/example for preferences_set to show
calling it with a single object whose "preferences" field is a JSON-stringified
map containing the ui_user_name key (i.e., JSON.stringify of { "ui_user_name":
"<name>" }) so the schema and the handler's JSON.parse will succeed;
alternatively, if you prefer API change, modify the preferences_set
handler/schema (the preferences parameter and its JSON.parse usage) to accept a
direct key/value payload, but keep the code and docs consistent (refer to
preferences_set, the "preferences" parameter and the JSON.parse call).
In `@config/clawbox-workspace-guide.md`:
- Around line 76-78: Add a language identifier to the fenced code block
containing the preferences_set invocation to satisfy the MD040 lint rule; locate
the fenced block that contains preferences_set("ui_user_name", "<name>") and
change the opening fence from ``` to ```text (or ```python) so the snippet is
fenced with a language identifier.
In `@src/app/setup-api/ai-models/catalog/route.ts`:
- Around line 169-172: The "openai-codex" regex in ALLOWED_MODEL_RE_BY_PROVIDER
is too permissive (the pattern /^gpt-5\.[45](-mini)?$/ also matches
gpt-5.5-mini); update the "openai-codex" entry so it only permits the explicit
SKUs allowed (gpt-5.5, gpt-5.4, and gpt-5.4-mini) by replacing its RegExp with
one that enumerates those variants (e.g., an alternation that matches gpt-5.5 or
gpt-5.4 with optional -mini) so the unsupported gpt-5.5-mini cannot pass
validation.
In `@src/app/setup-api/chat/model/route.ts`:
- Around line 179-194: The fallback logic in route.ts occasionally uses
defaultModelForProvider(rawProvider) which relies on the DEFAULT_PROVIDER_MODELS
map that still contains the old "openai/gpt-5" value; update the default to the
new curated OpenAI family id ("openai/gpt-5.4") so legacy/sparse configs pick
the correct pill. Locate DEFAULT_PROVIDER_MODELS (or the implementation of
defaultModelForProvider) and replace the OpenAI entry from "openai/gpt-5" to
"openai/gpt-5.4" (or have defaultModelForProvider return the new id for
rawProvider === "openai") ensuring providerDefinitions/definedModels logic
remains unchanged. Ensure tests or code paths that normalize provider/model
strings (normalizeProviderFromModel) still accept the new id.
In `@src/components/SettingsApp.tsx`:
- Around line 575-591: The polling still runs after the user edits because
tick()'s .finally() only checks cancelled, so add a guard for
userNameEditedRef.current there; inside tick()'s .finally() only schedule the
next setTimeout(tick, 5_000) when !cancelled && !userNameEditedRef.current (so
network requests stop once the user has edited), referencing the existing tick,
userNameEditedRef, cancelled and timer/setTimeout symbols to locate and update
the logic.
In `@src/tests/routes/ai-models/status.test.ts`:
- Around line 270-303: The test currently allows both local picker and portal to
return "pro", masking whether clawaiAccountTier comes from the portal; update
the test around the "returns clawaiAccountTier=pro alongside..." case so it
asserts the portal was consulted: after calling GET() assert fetchSpy was called
exactly once (or at least called) and/or change mockGetConfigValue to return a
distinct value (e.g. "free") while keeping the portal fetchSpy response
deviceTier:"pro" so the only way body.clawaiAccountTier === "pro" is if GET()
used the portal; reference the same mocks used in the test (mockGetConfigValue,
fetchSpy) and the GET() invocation when making the assertion or value change.
---
Outside diff comments:
In `@src/app/setup-api/ai-models/catalog/route.ts`:
- Around line 225-236: The payload currently forces allowCustom: true which
re-enables custom model IDs for providers like "clawai"; update
buildPayload(provider, models) to set allowCustom based on the provider catalog
config (e.g., use PROVIDER_CATALOGS[provider]?.allowCustom or a sensible
default) instead of hardcoding true, and apply the same change to the
empty/warming response path so both the real and fallback responses respect
PROVIDER_CATALOGS for the given provider (refer to buildPayload and the
warming/empty response construction).
In `@src/app/setup-api/ai-models/status/route.ts`:
- Around line 197-204: The provider comparison when selecting activeKey uses raw
strings (primaryProviderHint and entry?.provider) which misses aliases like
"deepseek" vs "clawai"; normalize both sides before comparing: add/use a
normalization function (e.g., normalizeProviderAlias) and call it on
primaryProviderHint and on entryProvider (derived from entry?.provider ??
key.split(":")[0]) inside the profileKeys.find in the activeKey assignment so
aliases map to the same canonical provider name while preserving the existing
fallback to profileKeys[0].
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 100a583a-b2b8-47a2-91b4-9ca56f314589
📒 Files selected for processing (16)
Clawbox.mdconfig/clawbox-workspace-guide.mde2e/helpers/clawbox.tssrc/app/globals.csssrc/app/page.tsxsrc/app/setup-api/ai-models/catalog/route.tssrc/app/setup-api/ai-models/status/route.tssrc/app/setup-api/chat/model/route.tssrc/components/ChatPopup.tsxsrc/components/HeaderDropdown.tsxsrc/components/SettingsApp.tsxsrc/lib/provider-models.tssrc/lib/translations.tssrc/lib/use-clawbox-login.tssrc/tests/routes/ai-models/status.test.tssrc/tests/unit/use-clawbox-login.test.ts
CodeRabbit findings (all major/minor in PR #124 review): * Clawbox.md / config/clawbox-workspace-guide.md: preferences_set takes a single JSON-string parameter, not two positional args. Examples updated to preferences_set('{"ui_user_name": "<name>"}'). Without this an agent following the docs would either fail schema validation or hit JSON.parse on a non-JSON string. Added text language hint to the workspace-guide fenced block to silence MD040. * catalog/route.ts ALLOWED_MODEL_RE_BY_PROVIDER: openai-codex regex /^gpt-5\.[45](-mini)?$/ accidentally allowed gpt-5.5-mini (not on the Codex auth path, would 400). Replaced with explicit alternation /^(?:gpt-5\.5|gpt-5\.4(?:-mini)?)$/. * catalog/route.ts allowCustom: was hardcoded to true in buildPayload and the warming-state empty payload, contradicting PROVIDER_CATALOGS.clawai.allowCustom = false. Added ALLOW_CUSTOM_BY_PROVIDER override consulted by both code paths so ClawBox AI cant accept arbitrary slugs Mike's gateway wouldnt route. * chat/model/route.ts DEFAULT_PROVIDER_MODELS.openai: still pointed at openai/gpt-5 even though the curated picker moved to gpt-5.4. Legacy configs without an explicit models.providers.openai.models block would surface a stale GPT pill. Updated to openai/gpt-5.4 to match. * SettingsApp.tsx user-name poll: tick().finally() rescheduled even after userNameEditedRef was set, leaving the network requests firing every 5s with results discarded. Added the ref check to .finally() so polling actually stops. * status.test.ts clawaiAccountTier-via-OpenAI test: mockGetConfigValue returned "pro" and the portal also returned deviceTier:"pro", so the test couldnt tell which source the route used. Local picker now returns "flash" (distinct), and we assert fetchSpy was called once to lock in that the portal must be consulted. Test updates for the consolidated chat picker (PR's own behavior change): * chat-model.test.ts "surfaces every model": ClawBox AI is now one row per provider, not one per model. Updated expected options to a single ClawBox AI entry with the active model on it; activeLabel drops the Pro/Flash suffix. Same shape change for "lists every configured cloud provider". * chat-model.test.ts default OpenAI model expected as openai/gpt-5.4 (was openai/gpt-5). * provider-models.test.ts default OpenAI model expected as gpt-5.4. * e2e/chat-popup.spec.ts: native selectOption() doesnt work on the new custom HeaderDropdown (it's button + popover, not <select>). Two tests updated to click the trigger by aria-label, then click the option by accessible name. Hygiene: * .scratch/ added to .gitignore — these are per-session debug scripts that shouldnt show up as untracked clutter or get accidentally committed by `git add -A`.
* status/route.ts:197-204 — primaryProviderHint comparison now flows through normalizeProvider so the deepseek/clawai alias collapses. Without this, a primary model of clawai/deepseek-v4-pro never matched a profile recorded under the wire-format `deepseek` provider, and we silently fell back to profileKeys[0]. Caught by CodeRabbit on the prior pass; missed in my first sweep. * e2e/chat-popup.spec.ts — dropped the Escape sanity-check between opening the provider dropdown and picking an option. The check was specific to native <select> behavior (escape dismisses the browser dropdown but keeps the page state). With the custom HeaderDropdown popover, Escape both closes the popover AND can be handled higher up (chat popup itself), so the trigger button stops being available for the second click. The test now just exercises open + select, which is the user-visible path.
Summary
Five connected fixes around the chat picker / account tier:
Account-tier independence — ClawKeep + Remote Desktop entitlement no longer flickers when switching active chat provider. A Max-subscription user chatting via OpenAI keeps paid features unlocked.
User-name field — placeholder removed, 5s preference poll gated by a "user is typing" ref, agent instruction to set
ui_user_namefrom conversation.Disk-backed catalog cache + background warmup —
openclaw models listis ~3min on Jetson Orin Nano. Route now serves fromdata/catalog-cache/<provider>.jsonwith 6h TTL, refreshes happen out-of-band. Boot warmup fires off refreshes for every catalog provider on first import. Picker opens instant after the first ~3min of uptime.Per-provider reasoning effort + curated openai picker — replaced the universal 9-level dropdown with per-provider sets sourced from each upstream's API docs. OpenAI / OpenAI-Codex pickers curated to the 5.4 + 5.5 family; openai-codex additionally hides -pro variants (ChatGPT-account auth rejects them).
Redesigned chat header — new
HeaderDropdownpopover component replaces native<select>(compact trigger that truncates with..., wider popover with full labels + hints). Three loose pills instead of a segmented bar. ClawBox AI consolidated into one provider entry with Flash/Pro selectable via the secondary model dropdown — matches how Anthropic, OpenAI, etc work.Files changed
src/app/setup-api/ai-models/status/route.ts— splitclawaiTier(active) fromclawaiAccountTier(any profile)src/app/setup-api/ai-models/catalog/route.ts— disk cache, background refresh, per-provider allowlist regex, clawai static catalogsrc/app/setup-api/chat/model/route.ts— options keyed by provider (one row per provider), normalized provider checksrc/components/HeaderDropdown.tsx(new) — custom popover dropdownsrc/components/ChatPopup.tsx— per-provider reasoning levels, deepseek alias handling, header pillssrc/lib/use-clawbox-login.ts— account-level vs active-provider tiersrc/lib/provider-models.ts—CLAWAI_MODELS, expanded openai-codex fallback,clawaiinCATALOG_PROVIDERSClawbox.md,config/clawbox-workspace-guide.md— agent instruction forui_user_namesrc/lib/translations.ts— dropsettings.userName.placeholderfrom 10 localessrc/tests/routes/ai-models/status.test.ts+src/tests/unit/use-clawbox-login.test.tsTest plan
deepseek/<model>(no "Selected AI provider is not configured" error)clawbox-setup→ first picker open shows static fallback briefly, ~3min later shows live catalog from disk cache