Skip to content

Comments

fix(ui): 服务商表单小屏体验与客户端限制配置 [未完成]#816

Open
tesgth032 wants to merge 23 commits intoding113:devfrom
tesgth032:fix/issue-799-mobile-ui
Open

fix(ui): 服务商表单小屏体验与客户端限制配置 [未完成]#816
tesgth032 wants to merge 23 commits intoding113:devfrom
tesgth032:fix/issue-799-mobile-ui

Conversation

@tesgth032
Copy link
Contributor

@tesgth032 tesgth032 commented Feb 23, 2026

Summary

  • Fixes 新增服务商没有保存按钮??? #799:修复 Provider 设置表单在小屏/移动端的可用性问题(底部操作区遮挡、滚动/高度抖动等)。
  • 统一视口高度策略(CSS 变量 + dvh fallback + safe-area),并在多个页面/对话框/抽屉替换混用的 vh/dvh。
  • Provider 表单新增“客户端限制”配置:默认不限制;开启后仅配置白名单/黑名单(黑名单优先),支持预设客户端与自定义 UA/匹配模式。
  • 修复 standalone 本地启动(node .next/standalone/server.js)缺失静态资源导致样式加载失败的问题(补齐 .next/staticpublic)。

Changes

Providers UI/UX

  • 小屏:Tab 导航与步骤指示适配移动端 safe-area,避免遮挡提交按钮;交互/ARIA 补充。
  • 模型白名单:选择器与摘要文案分组;小屏 Popover 防溢出。
  • Anthropic 覆写:Max Tokens/思考预算/自适应思考下拉在“仅剩选择器”时自适应占满可用宽度,避免文本截断;Tooltip 交互保持原设计。
  • 客户端限制:开关控制显隐;统一编辑器(白/黑名单 + 自定义 UA);互斥去重与输入提示(黑名单优先)。

Viewport / safe-area

  • 新增 --cch-viewport-height*.safe-area-bottom,并在多处布局/弹层改用变量驱动的 height/max-height,减少移动端地址栏伸缩造成的抖动。

Infra / Tests / Redis

  • i18n:补齐相关新增文案(5 语言)。
  • E2E:改为通过登录换取会话 token,提高稳定性。
  • Redis:连接选项与关闭流程加固。
  • Standalone:构建后复制 VERSION + 静态资源到 .next/standalone

Testing

  • bun run build
  • bun run lint
  • bun run lint:fix
  • bun run typecheck
  • bun run test

Greptile Summary

This PR is a large, multi-faceted changeset that addresses mobile/small-screen UX issues in the Provider settings form, introduces a unified viewport height strategy via CSS variables, adds a new "client restrictions" toggle+editor feature, and improves infrastructure (Redis client lifecycle, standalone build, E2E auth, test stability).

  • Viewport height unification: Introduces --cch-viewport-height* CSS variables in globals.css with dvh fallback via @supports, then migrates ~30 dialog/layout files from hardcoded vh/dvh to the new variables. Adds .safe-area-bottom utility for iOS home indicator padding.
  • Client restrictions redesign: Replaces the old checkbox-based preset UI with a toggle-gated ClientRestrictionsEditor component using TagInput with suggestion support and mutual exclusion logic (adding to allowed auto-removes from blocked and vice versa).
  • Provider form mobile UX: Reorders tab nav below content on mobile via order utilities, changes mobile nav from fixed to relative positioning, adds ARIA progressbar attributes, and applies safe-area-bottom padding.
  • Model multi-select: Extracts ModelSourceIndicator to a standalone component, memoizes displayedModels, and adds max-w-[calc(100vw-2rem)] to prevent Popover overflow on small screens.
  • Redis client hardening: Reuses buildRedisOptionsForUrl in getRedisClient, adds stale client detection via status === "end", an end event handler for auto-reset, and a robust closeRedis with error fallback to disconnect().
  • E2E auth: New loginAndGetAuthToken helper that logs in via API and extracts session cookies, replacing direct token usage. All E2E tests migrated to this flow.
  • Standalone build: copy-version-to-standalone.cjs now also copies .next/static and public to the standalone output.
  • Test stability: Calendar range tests use fixed dates, circuit breaker tests await async alert tasks, thinking budget editor tests query buttons more precisely, and Vitest maxWorkers is now configurable.
  • i18n: Hardcoded Chinese status labels in key-list-header.tsx replaced with translation keys across all 5 languages. New client restriction toggle/description strings added.

Confidence Score: 4/5

  • This PR is generally safe to merge — the changes are well-structured and the core logic is sound, though the [未完成] tag in the title suggests it may not be fully ready.
  • The viewport migration is mechanical and low-risk. The client restrictions editor has clean mutual exclusion logic. The Redis improvements add proper lifecycle management. E2E auth migration is reasonable. Most issues flagged in previous threads are style/optimization suggestions rather than correctness bugs. The PR title contains [未完成] (incomplete), suggesting the author considers it work-in-progress.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx (animation disabled), tests/e2e/_helpers/auth.ts (retry/backoff concerns from previous threads), src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (client restrictions toggle state management)

Important Files Changed

Filename Overview
src/components/form/client-restrictions-editor.tsx New component providing unified allowlist/blocklist tag editor with mutual exclusion logic. Clean implementation, minor note: uniqueOrdered trims values while the version in client-presets.ts does not.
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx Refactored client restrictions from checkbox-based preset UI to toggle+TagInput editor. Toggle state synced via useEffect.
src/lib/redis/client.ts Improved Redis client lifecycle: reuses buildRedisOptionsForUrl, adds stale client detection, end event handler, and robust closeRedis with error fallback to disconnect.
src/app/globals.css Adds --cch-viewport-height CSS variables with dvh fallback via @supports, plus .safe-area-bottom utility. Clean approach for mobile viewport stability.
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx Migrates form to use CSS viewport variable, reorders tab nav below content on mobile with order utilities, removes bottom padding workaround.
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx Mobile nav changed from fixed to relative positioning with safe-area padding. Added ARIA progressbar. Animation effectively disabled (initial === animate).
tests/e2e/_helpers/auth.ts New E2E auth helper that logs in via API and extracts auth-token from Set-Cookie headers. Has retry logic for transient failures.
src/components/ui/tag-input.tsx Fixed click-outside handler to use capture phase, preventing false close when suggestion items are removed by React during mousedown.
src/app/[locale]/settings/providers/_components/model-multi-select.tsx Extracted SourceIndicator to standalone component, memoized displayedModels, added max-width overflow protection for mobile Popover.
scripts/copy-version-to-standalone.cjs Adds copying of .next/static and public directories to standalone output for local server.js execution.
src/app/[locale]/dashboard/_components/user/key-list-header.tsx Replaced hardcoded Chinese status labels with i18n-driven translation keys. Clean refactor with proper type safety.
src/app/global-error.tsx Added globals.css import and migrated minHeight from 100vh to CSS viewport variable with fallback. Appropriate for global error boundary.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["globals.css<br/>--cch-viewport-height* variables<br/>dvh fallback via @supports"] --> B["Layouts & Pages<br/>min-h-[var(--cch-viewport-height)]"]
    A --> C["Dialogs & Drawers<br/>max-h-[var(--cch-viewport-height-*)]"]
    A --> D["Provider Form<br/>max-h-[var(--cch-viewport-height-85)]"]
    
    D --> E["FormTabNav<br/>Mobile: relative + safe-area-bottom<br/>Desktop: sidebar"]
    D --> F["RoutingSection"]
    
    F --> G{"Client Restrictions<br/>Toggle Enabled?"}
    G -- Yes --> H["ClientRestrictionsEditor"]
    H --> I["TagInput (Allowed)<br/>with preset suggestions"]
    H --> J["TagInput (Blocked)<br/>with preset suggestions"]
    I -- "mutual exclusion" --> J
    J -- "mutual exclusion" --> I
    G -- "No (disable)" --> K["Clear both lists<br/>SET_ALLOWED_CLIENTS: []<br/>SET_BLOCKED_CLIENTS: []"]

    L["E2E Tests"] --> M["loginAndGetAuthToken<br/>POST /api/auth/login"]
    M --> N["Extract auth-token<br/>from Set-Cookie"]
    N --> O["API calls with<br/>Bearer + Cookie auth"]
Loading

Last reviewed commit: a7573cb

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

将大量固定 vh/dvh 高度改为基于新 CSS 变量 --cch-viewport-height-* 的计算并添加 safe-area 支持;providers 表单移动端导航与步骤进度更新;新增 ClientRestrictionsEditor;E2E 改为登录令牌流程;Redis 客户端类型与生命周期重构。

Changes

Cohort / File(s) Summary
Dialog/Drawer/Sheet 样式
src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx, src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx, src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx, src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx, src/components/ui/drawer.tsx, src/components/ui/sheet.tsx, src/components/ui/...
大量将 `max-h-[..vh
全局 CSS:视口变量与 safe-area
src/app/globals.css
新增 --cch-viewport-height-* 系列变量并在 @supports (height:100dvh) 下使用 dvh 版本;新增 .safe-area-bottom 实用类以支持 iOS 安全区。
页面/容器高度替换
src/app/[locale]/*/layout.tsx, src/app/[locale]/*/page.tsx, src/app/[locale]/*/login/*, src/app/[locale]/*/dashboard/*, src/app/[locale]/*/usage-doc/*, src/app/global-error.tsx, src/app/[locale]/internal/dashboard/*
min-h-screen/h-screen/calc(100vh...) 等替换为基于 var(--cch-viewport-height,100vh) 的类,统一视口高度来源。
Providers 表单:选项卡、移动导航与进度
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
移动端底部导航从 fixed 改为 相对容器;新增 activeTabIndex/stepProgress 计算与 ARIA 属性;底部步骤进度条使用动态宽度并集成可访问属性;调整 tab 在 flex 中的 order。
Client Restrictions 新增与重构
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx, src/components/form/client-restrictions-editor.tsx
移除散落的预设/自定义处理,新增并导出 ClientRestrictionsEditor(含内部 ListEditor),通过开关统一管理 allowed/blocked 列表并处理互斥逻辑(新增导出类型与组件)。
Redis 客户端重构
src/lib/redis/client.ts
缩窄 TLS 类型签名,buildRedisOptionsForUrl 改为返回 { isTLS, options },调整客户端创建/复用、事件绑定及 end/close 清理逻辑,导出签名有细微变化。
E2E:登录与认证改造
tests/e2e/_helpers/auth.ts, tests/e2e/api-complete.test.ts, tests/e2e/notification-settings.test.ts, tests/e2e/users-keys-complete.test.ts
新增 loginAndGetAuthToken,E2E 测试改为通过 ADMIN_KEY 登录获取会话 token;API 调用使用 Bearer header 与 cookie;测试在无凭据时可选择跳过或报错。
Vitest 配置
vitest.config.ts
新增 parsePositiveInt,并通过环境变量 VITEST_MAX_WORKERS 配置 maxWorkers(默认 8)。
测试与 Mock 调整
tests/unit/lib/database-backup/docker-executor.test.ts, tests/unit/lib/endpoint-circuit-breaker.test.ts, tests/unit/login/login-visual-regression.test.tsx
简化对 node:child_process/node:fs 的测试 mock;新增等待辅助 waitForMockCalled 并替换部分轮询;视觉回归测试选择器与高度断言更新。
i18n 文案扩展
messages/*/dashboard.json, messages/*/settings/providers/batchEdit.json, messages/*/settings/providers/form/*.json
为 keyListHeader 添加 userStatus 四种状态译文;batchEdit 增加 nullValue;providers 表单添加 stepProgress 与 clientRestrictions 文案,多语言同步更新。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ding113
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.44% 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
Linked Issues check ✅ Passed PR#816的代码变更充分满足Issue#799的需求:修复了移动端/小屏幕下服务商表单提交按钮被遮挡的问题。
Out of Scope Changes check ✅ Passed 所有变更均围绕Issue#799的核心目标(视口高度策略、表单布局、安全区处理)及相关基础设施改进(i18n、Redis、测试)展开,无超出范围的变更。
Title check ✅ Passed 标题用中文描述了修复服务商表单小屏/移动端体验的主要改动,与变更集的核心内容直接相关,尽管标题较为简洁。
Description check ✅ Passed 拉取请求描述与变更集高度相关,涵盖了所有主要改动:视口高度策略统一、客户端限制功能、移动端适配、基础设施改进等方面。

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @tesgth032, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求主要解决了服务商表单在小屏和移动设备上的自适应问题,特别是修复了提交按钮被底部导航遮挡的缺陷。通过调整表单布局和统一对话框内容样式,提升了用户体验。此外,还包含了对单元测试配置的改进,以增强测试的稳定性和兼容性。

Highlights

  • 响应式布局优化: 修复了服务商表单在小屏/移动端布局问题,确保提交按钮不再被底部导航遮挡。
  • UI样式统一: 统一了 ProviderForm 相关对话框的 DialogContent 样式,以避免双层 padding 和高度挤压问题。
  • 测试稳定性改进: 优化了 docker-executor 单测中 Node 内置模块的 mock 方式,提升测试稳定性。
  • Vitest并发配置: 为 Vitest 配置了 maxWorkers 限制,减少高核/Windows 环境下的资源争用和测试超时风险。
Changelog
  • src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx
    • 统一 DialogContent 样式,添加 overflow-hidden, p-0, gap-0 类。
  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
    • 调整移动端底部导航样式,移除 fixed 定位,改为 relativeshrink-0
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
    • 调整 FormTabNav 的布局顺序,使其在小屏下位于内容下方。
    • 修改内容区域的布局顺序和底部填充,以适应新的响应式设计。
  • src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx
    • 统一编辑和克隆对话框的 DialogContent 样式,并调整最大高度。
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
    • 为编辑和克隆对话框的 DialogContent 添加 overflow-hidden, p-0, gap-0 类。
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
    • 统一添加和编辑供应商密钥对话框的 DialogContent 样式。
  • tests/unit/lib/database-backup/docker-executor.test.ts
    • 重构 Vitest 对 node:child_processnode:fs 模块的 mock 方式,以解决加载错误。
  • vitest.config.ts
    • 新增 parsePositiveInt 辅助函数。
    • 为 Vitest 配置 maxWorkers,默认值为 8,并支持通过环境变量覆盖。
Activity
  • 此拉取请求旨在解决移动端表单显示问题并优化测试配置。
  • 目前没有其他评论或审查活动。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added bug Something isn't working area:UI area:provider labels Feb 23, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +556 to +564
<div className="order-2 md:order-1 shrink-0">
{/* Tab Navigation */}
<FormTabNav
activeTab={state.ui.activeTab}
onTabChange={handleTabChange}
disabled={isPending}
tabStatus={getTabStatus()}
/>
</div>
Copy link

Choose a reason for hiding this comment

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

On small screens (< md breakpoint), the nav appears below content due to order-2 md:order-1, but on mobile the nav should typically be at the bottom for thumb accessibility. Check that this ordering matches the intended UX.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 556-564

Comment:
On small screens (< `md` breakpoint), the nav appears below content due to `order-2 md:order-1`, but on mobile the nav should typically be at the bottom for thumb accessibility. Check that this ordering matches the intended UX.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

本次 PR 主要包含两方面的改进:修复服务商表单在小屏幕/移动端的自适应布局问题,以及提升测试的稳定性。在 UI 方面,通过将之前在小屏幕上遮挡内容区域的固定底部导航栏,调整为文档流的一部分,成功解决了提交按钮被遮挡的问题。同时,统一了多个组件中 DialogContent 的样式,采用 p-0overflow-hidden 等一致的布局属性,避免了双层内边距等问题,提升了布局的统一性和可维护性。使用 Flexbox 的 order 属性进行响应式布局调整,是一个清晰且有效的实现方式。在测试稳定性方面,简化了 docker-executor.test.ts 中对 Node.js 内置模块的 mock 方式,这有助于避免在某些 Vitest 环境下因模块提升(hoisting)而导致的加载错误。此外,更新了 Vitest 配置,允许通过环境变量设置 maxWorkers,从而更好地控制测试期间的资源占用。总体而言,这些变更是目标明确、实现清晰的改进,同时提升了用户体验和开发环境的稳定性。代码质量很高,我没有发现需要修改的问题。

@github-actions github-actions bot added the size/S Small PR (< 200 lines) label Feb 23, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR addresses a mobile/small-screen UI issue where the provider form submit button was being obscured by the fixed bottom navigation. The changes are well-targeted and minimal.

PR Size: S

  • Lines changed: 63 (35 additions, 28 deletions)
  • Files changed: 8

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Change Analysis

UI Layout Fix (Primary Change)

  • form-tab-nav.tsx:214: Changed mobile navigation from fixed bottom-0 left-0 right-0 z-50 to shrink-0 relative - correctly removes the fixed positioning that was covering the submit button
  • provider-form/index.tsx:553-570: Wrapped FormTabNav in a div with order-2 md:order-1 and changed content area to order-1 md:order-2 - this properly reorders the layout for mobile while maintaining desktop order
  • Removed pb-24 md:pb-6 padding from content area since fixed positioning is no longer used

Dialog Consistency

  • 6 files: Added overflow-hidden p-0 gap-0 to DialogContent for consistent padding/overflow handling across all provider dialogs

Test Infrastructure

  • docker-executor.test.ts: Simplified mock pattern by removing importOriginal, which resolves Vitest compatibility issues
  • vitest.config.ts: Added parsePositiveInt helper and maxWorkers config to limit worker count (defaults to 8, configurable via VITEST_MAX_WORKERS)

Validation Performed

  • Verified parsePositiveInt handles all edge cases (undefined, NaN, non-positive values)
  • Confirmed the layout reordering logic is correct for both mobile and desktop viewports
  • Checked that the mock changes don't affect test coverage

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (CSS changes, test infrastructure improved)
  • Code clarity - Good

Automated review by Claude AI

Copy link

@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: 1

🧹 Nitpick comments (2)
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx (2)

268-268: findIndex 回调参数 t 遮蔽了外层翻译函数 t

第 32 行定义了 const t = useTranslations("settings.providers.form"),而此处 TAB_CONFIG.findIndex((t) => t.id === activeTab) 中的参数 t 遮蔽了外层的翻译函数。虽然当前逻辑正确(t 在此 lambda 内未被调用为翻译函数),但容易造成误解,建议改用更具描述性的参数名。

建议修复
-              width: `${((TAB_CONFIG.findIndex((t) => t.id === activeTab) + 1) / TAB_CONFIG.length) * 100}%`,
+              width: `${((TAB_CONFIG.findIndex((tab) => tab.id === activeTab) + 1) / TAB_CONFIG.length) * 100}%`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
at line 268, The findIndex callback parameter shadows the outer translation
function const t = useTranslations("settings.providers.form"); update
TAB_CONFIG.findIndex((t) => t.id === activeTab) to use a descriptive name (e.g.,
tab or cfg) to avoid shadowing and confusion—locate the expression in
form-tab-nav.tsx where width is computed and replace the callback parameter name
while keeping the same comparison against activeTab.

263-272: 进度条初始宽度硬编码为 "20%",与 TAB 数量耦合

initial={{ width: "20%" }} 硬编码对应 5 个 Tab(1/5 = 20%)。若未来 TAB_CONFIG 数量变化,初始动画值将不匹配,导致组件首次挂载时出现不必要的动画跳跃(从错误的初始值动画到正确目标值)。

建议修复:动态计算初始宽度
-            initial={{ width: "20%" }}
+            initial={{ width: `${(1 / TAB_CONFIG.length) * 100}%` }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
around lines 263 - 272, The progress bar's initial width is hardcoded to "20%"
causing jump when TAB_CONFIG length changes; update the motion.div initial width
to compute dynamically using TAB_CONFIG and activeTab (e.g., find the index via
TAB_CONFIG.findIndex(t => t.id === activeTab) and set initial width to
((indexFound >= 0 ? indexFound + 1 : 0) / TAB_CONFIG.length) * 100 + '%') so
initial and animate widths match; reference the motion.div, TAB_CONFIG and
activeTab when making this change and ensure you handle index not found fallback
to 0%.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx:
- Line 214: The custom class safe-area-bottom used in components like
form-tab-nav (component FormTabNav) and settings-nav is not defined in Tailwind
or globals.css, so add a utility definition in src/app/globals.css inside `@layer`
utilities that defines .safe-area-bottom to apply bottom padding using the iOS
safe-area inset (e.g. padding-bottom: env(safe-area-inset-bottom); with a
sensible fallback), then rebuild so Tailwind includes the new utility.

---

Nitpick comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx:
- Line 268: The findIndex callback parameter shadows the outer translation
function const t = useTranslations("settings.providers.form"); update
TAB_CONFIG.findIndex((t) => t.id === activeTab) to use a descriptive name (e.g.,
tab or cfg) to avoid shadowing and confusion—locate the expression in
form-tab-nav.tsx where width is computed and replace the callback parameter name
while keeping the same comparison against activeTab.
- Around line 263-272: The progress bar's initial width is hardcoded to "20%"
causing jump when TAB_CONFIG length changes; update the motion.div initial width
to compute dynamically using TAB_CONFIG and activeTab (e.g., find the index via
TAB_CONFIG.findIndex(t => t.id === activeTab) and set initial width to
((indexFound >= 0 ? indexFound + 1 : 0) / TAB_CONFIG.length) * 100 + '%') so
initial and animate widths match; reference the motion.div, TAB_CONFIG and
activeTab when making this change and ensure you handle index not found fallback
to 0%.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +270 to +271
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
Copy link

Choose a reason for hiding this comment

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

initial should animate from current width, not jump to target width. Setting both to same value disables entry animation.

Suggested change
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
initial={{ width: "0%" }}
animate={{ width: stepProgressWidth }}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
Line: 270-271

Comment:
`initial` should animate from current width, not jump to target width. Setting both to same value disables entry animation.

```suggestion
            initial={{ width: "0%" }}
            animate={{ width: stepProgressWidth }}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@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: 1

🧹 Nitpick comments (3)
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx (2)

45-47: stepProgressWidthhorizontal 布局下被无效计算

layout === "horizontal" 时组件在第 49 行提前返回,但这三个变量在每次渲染时都会被计算。建议将此段逻辑移入 vertical 分支内部,或包裹在 layout !== "horizontal" 条件中,避免冗余运算。

♻️ 建议重构
- const activeTabIndex = TAB_CONFIG.findIndex((tab) => tab.id === activeTab);
- const stepNumber = activeTabIndex >= 0 ? activeTabIndex + 1 : 0;
- const stepProgressWidth = `${(stepNumber / TAB_CONFIG.length) * 100}%`;

  if (layout === "horizontal") {
    return ( ... );
  }

+ const activeTabIndex = TAB_CONFIG.findIndex((tab) => tab.id === activeTab);
+ const stepNumber = activeTabIndex >= 0 ? activeTabIndex + 1 : 0;
+ const stepProgressWidth = `${(stepNumber / TAB_CONFIG.length) * 100}%`;

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

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
around lines 45 - 47, The computed values activeTabIndex, stepNumber and
stepProgressWidth are always calculated even when layout === "horizontal" (where
the component returns early); move the logic that derives activeTabIndex,
stepNumber and stepProgressWidth into the vertical branch (or wrap them with an
if (layout !== "horizontal") guard) so those values are only computed when
needed—use the existing TAB_CONFIG and activeTab symbols to locate and relocate
the calculations (or wrap them) near the rendering logic that handles the
vertical layout.

270-271: initialanimate 值相同,mount 动画被抑制

initial 在首次挂载时仅生效一次;因其值与 animate 完全相同,首次渲染不会产生入场动画。若希望进度条从 0% 滑入当前位置,应将 initial 改为 "0%";若刻意跳过入场动画,则可直接删除 initial 属性(framer-motion 默认行为与此等效)。

♻️ 可选调整(带入场动画)
  <motion.div
    className="h-full bg-primary"
-   initial={{ width: stepProgressWidth }}
+   initial={{ width: "0%" }}
    animate={{ width: stepProgressWidth }}
    transition={{ type: "spring", stiffness: 300, damping: 30 }}
  />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
around lines 270 - 271, The component sets the Framer Motion props initial and
animate both to the same value (stepProgressWidth), so the mount animation is
suppressed; change initial to a fixed starting value (e.g., "0%") to animate the
progress bar in from zero, or remove the initial prop entirely if you want to
skip the entrance animation—update the JSX where initial={{ width:
stepProgressWidth }} / animate={{ width: stepProgressWidth }} (reference:
stepProgressWidth in form-tab-nav.tsx) accordingly.
src/app/globals.css (1)

138-141: Tailwind v4 中自定义工具类应使用 @utility 而非 @layer utilities

在 Tailwind v4 中,@layer utilities 不再由 Tailwind 特殊处理,因此定义在其中的类不会支持 hover:focus: 或响应式前缀(如 md:)等变体。应改用 @utility 指令以确保变体正常工作。

该类已在以下位置使用:

  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx:218(无固定定位)
  • src/app/[locale]/settings/_components/settings-nav.tsx:222(带 fixed bottom-0

建议按照 Tailwind v4 最佳实践改为 @utility 语法:

♻️ 建议使用 `@utility` 指令
-  /* iOS 安全区:避免底部固定导航被 Home Indicator 挡住 */
-  .safe-area-bottom {
-    padding-bottom: env(safe-area-inset-bottom, 0px);
-  }
+/* iOS 安全区:为底部内容提供 Home Indicator 安全间距 */
+@utility safe-area-bottom {
+  padding-bottom: env(safe-area-inset-bottom, 0px);
+}

注:@utility 应置于 @layer utilities 块之外,与 @layer base 同级。

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

In `@src/app/globals.css` around lines 138 - 141, The custom .safe-area-bottom
utility is defined in a plain CSS layer so Tailwind v4 won't apply
hover/focus/responsive variants; move its definition out of any `@layer` utilities
block and convert it to a Tailwind `@utility` declaration (placed at the same
level as `@layer` base) that sets padding-bottom: env(safe-area-inset-bottom,
0px); this ensures variant support for usages in components such as
form-tab-nav.tsx (line with no fixed positioning) and settings-nav.tsx (where
it’s used with fixed bottom-0).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx:
- Around line 267-274: The visual progress indicator in form-tab-nav.tsx (the
outer div + motion.div used for step progress) lacks accessibility semantics;
update the element representing progress (the motion.div with className "h-full
bg-primary") to include role="progressbar" and ARIA attributes aria-valuenow
(current step progress value), aria-valuemin="0" and aria-valuemax="100", and a
descriptive aria-label or aria-labelledby so screen readers can announce
progress; ensure the code reads the same stepProgressWidth/value used for the
animated width to compute aria-valuenow.

---

Nitpick comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx:
- Around line 45-47: The computed values activeTabIndex, stepNumber and
stepProgressWidth are always calculated even when layout === "horizontal" (where
the component returns early); move the logic that derives activeTabIndex,
stepNumber and stepProgressWidth into the vertical branch (or wrap them with an
if (layout !== "horizontal") guard) so those values are only computed when
needed—use the existing TAB_CONFIG and activeTab symbols to locate and relocate
the calculations (or wrap them) near the rendering logic that handles the
vertical layout.
- Around line 270-271: The component sets the Framer Motion props initial and
animate both to the same value (stepProgressWidth), so the mount animation is
suppressed; change initial to a fixed starting value (e.g., "0%") to animate the
progress bar in from zero, or remove the initial prop entirely if you want to
skip the entrance animation—update the JSX where initial={{ width:
stepProgressWidth }} / animate={{ width: stepProgressWidth }} (reference:
stepProgressWidth in form-tab-nav.tsx) accordingly.

In `@src/app/globals.css`:
- Around line 138-141: The custom .safe-area-bottom utility is defined in a
plain CSS layer so Tailwind v4 won't apply hover/focus/responsive variants; move
its definition out of any `@layer` utilities block and convert it to a Tailwind
`@utility` declaration (placed at the same level as `@layer` base) that sets
padding-bottom: env(safe-area-inset-bottom, 0px); this ensures variant support
for usages in components such as form-tab-nav.tsx (line with no fixed
positioning) and settings-nav.tsx (where it’s used with fixed bottom-0).

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

13 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +18 to +21
return combined
.split(/,(?=[^;]+?=)/g)
.map((s) => s.trim())
.filter(Boolean);
Copy link

Choose a reason for hiding this comment

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

Regex /,(?=[^;]+?=)/g may incorrectly split Set-Cookie headers containing dates (e.g., Expires=Wed, 21 Oct 2026). The lookahead (?=[^;]+?=) prevents splitting at commas within individual cookie attributes, but date values like Expires=Wed, 21 Oct 2026 will still match because "21" is followed by =.

Suggested change
return combined
.split(/,(?=[^;]+?=)/g)
.map((s) => s.trim())
.filter(Boolean);
// Use getSetCookie() if available, otherwise parse manually
// Note: Splitting Set-Cookie headers is complex due to date formats
return combined
.split(/,\s*(?=[a-zA-Z][\w-]*=)/g)
.map((s) => s.trim())
.filter(Boolean);
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 18-21

Comment:
Regex `/,(?=[^;]+?=)/g` may incorrectly split Set-Cookie headers containing dates (e.g., `Expires=Wed, 21 Oct 2026`). The lookahead `(?=[^;]+?=)` prevents splitting at commas within individual cookie attributes, but date values like `Expires=Wed, 21 Oct 2026` will still match because "21" is followed by `=`.

```suggestion
  // Use getSetCookie() if available, otherwise parse manually
  // Note: Splitting Set-Cookie headers is complex due to date formats
  return combined
    .split(/,\s*(?=[a-zA-Z][\w-]*=)/g)
    .map((s) => s.trim())
    .filter(Boolean);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +556 to +564
<div className="order-2 md:order-1 shrink-0">
{/* Tab Navigation */}
<FormTabNav
activeTab={state.ui.activeTab}
onTabChange={handleTabChange}
disabled={isPending}
tabStatus={getTabStatus()}
/>
</div>
Copy link

Choose a reason for hiding this comment

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

Wrapping FormTabNav in a separate div with shrink-0 prevents the nav from being a direct flex child of the parent. This adds an extra DOM node that may not be necessary.

Suggested change
<div className="order-2 md:order-1 shrink-0">
{/* Tab Navigation */}
<FormTabNav
activeTab={state.ui.activeTab}
onTabChange={handleTabChange}
disabled={isPending}
tabStatus={getTabStatus()}
/>
</div>
{/* Tab Navigation */}
<FormTabNav
className="order-2 md:order-1 shrink-0"
activeTab={state.ui.activeTab}
onTabChange={handleTabChange}
disabled={isPending}
tabStatus={getTabStatus()}
/>

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 556-564

Comment:
Wrapping `FormTabNav` in a separate `div` with `shrink-0` prevents the nav from being a direct flex child of the parent. This adds an extra DOM node that may not be necessary.

```suggestion
        {/* Tab Navigation */}
        <FormTabNav
          className="order-2 md:order-1 shrink-0"
          activeTab={state.ui.activeTab}
          onTabChange={handleTabChange}
          disabled={isPending}
          tabStatus={getTabStatus()}
        />
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 30 to +36

// ==================== 辅助函数 ====================

beforeAll(async () => {
if (!ADMIN_KEY) return;
authToken = await loginAndGetAuthToken(API_BASE_URL, ADMIN_KEY);
});
Copy link

Choose a reason for hiding this comment

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

Using beforeAll for async setup means if login fails, subsequent tests will fail with unclear errors about missing authToken. The error message on line 40-41 helps, but tests will still run and fail individually.

Consider using beforeAll with a skip condition or making the login failure more visible.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/api-complete.test.ts
Line: 30-36

Comment:
Using `beforeAll` for async setup means if login fails, subsequent tests will fail with unclear errors about missing `authToken`. The error message on line 40-41 helps, but tests will still run and fail individually.

Consider using `beforeAll` with a skip condition or making the login failure more visible.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@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: 1

🧹 Nitpick comments (2)
tests/e2e/users-keys-complete.test.ts (1)

37-37: afterAll 声明在 beforeAll 之前,且存在 sessionToken/authToken 双重命名

存在两个小问题:

  1. 声明顺序:模块级别的 afterAll(第 171 行)出现在 beforeAll(第 192 行)之前。虽然 Vitest 执行时始终先运行 beforeAll 再运行 afterAll,与代码顺序无关,但与 api-complete.test.tsnotification-settings.test.ts 中"先 beforeAllafterAll"的惯例不一致,会给读者造成困惑。

  2. 命名不一致:本文件模块变量命名为 sessionToken,但 callApi 的参数命名为 authToken(默认值为 sessionToken),在同一文件内形成双重命名,可统一为 sessionToken

♻️ 建议调整声明顺序并统一命名

beforeAll 移至 afterAll 之前(与其他测试文件对齐):

+beforeAll(async () => {
+  if (!ADMIN_KEY) return;
+  sessionToken = await loginAndGetAuthToken(API_BASE_URL, ADMIN_KEY);
+});
+
 afterAll(async () => {
   if (!sessionToken) return;
   // ... cleanup ...
 });
-
-beforeAll(async () => {
-  if (!ADMIN_KEY) return;
-  sessionToken = await loginAndGetAuthToken(API_BASE_URL, ADMIN_KEY);
-});

统一 callApi 参数命名:

 async function callApi(
   module: string,
   action: string,
   body: Record<string, unknown> = {},
-  authToken = sessionToken
+  sessionToken: string | undefined = sessionToken
 ) {
-  if (!authToken) {
+  if (!sessionToken) {
     throw new Error("E2E tests require ADMIN_TOKEN/TEST_ADMIN_TOKEN (used to login)");
   }
   const response = await fetch(url, {
     headers: {
-      Authorization: `Bearer ${authToken}`,
-      Cookie: `auth-token=${authToken}`,
+      Authorization: `Bearer ${sessionToken}`,
+      Cookie: `auth-token=${sessionToken}`,
     },
   });

Also applies to: 65-65, 172-172, 192-195

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

In `@tests/e2e/users-keys-complete.test.ts` at line 37, Move the module-level
beforeAll so it appears before the module-level afterAll (make the order match
other tests) and rename the inconsistent authToken identifier to sessionToken:
update the callApi function signature/default to use sessionToken and replace
all authToken uses with sessionToken, ensuring the module variable let
sessionToken: string | undefined; is the single source of truth and that
afterAll (cleanup) stays after the now-earlier beforeAll setup.
tests/e2e/api-complete.test.ts (1)

16-17: callApi/beforeAll 认证逻辑在三个 E2E 测试文件中重复

callApi(含 Bearer + Cookie 头)和 beforeAll 登录流程在 api-complete.test.tsnotification-settings.test.tsusers-keys-complete.test.ts 中几乎完全相同,可考虑将其提取到 tests/e2e/_helpers/api.ts,减少日后维护负担。

Also applies to: 33-36, 47-48

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

In `@tests/e2e/api-complete.test.ts` around lines 16 - 17, Extract the duplicated
authentication and request helper into a shared module: create
tests/e2e/_helpers/api.ts that exports a callApi function which builds requests
with Bearer + Cookie headers and a helper to perform the beforeAll login flow
(reusing loginAndGetAuthToken) so tests can call that in their beforeAll; then
update api-complete.test.ts, notification-settings.test.ts, and
users-keys-complete.test.ts to import and use callApi and the shared
beforeAll/login helper instead of duplicating the logic in each file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/_helpers/auth.ts`:
- Around line 24-27: The extractCookieValue function builds a RegExp from the
cookieName which can lead to ReDoS or incorrect matches if cookieName contains
regex metacharacters; change extractCookieValue to parse the Set-Cookie string
with string operations instead: trim the header, split on ';' to get individual
cookie segments, find the segment that starts with cookieName + '=' (or compare
the segment name using substring/indexOf), then return the value via substring
or split('=') instead of using new RegExp; update references to
extractCookieValue accordingly.

---

Nitpick comments:
In `@tests/e2e/api-complete.test.ts`:
- Around line 16-17: Extract the duplicated authentication and request helper
into a shared module: create tests/e2e/_helpers/api.ts that exports a callApi
function which builds requests with Bearer + Cookie headers and a helper to
perform the beforeAll login flow (reusing loginAndGetAuthToken) so tests can
call that in their beforeAll; then update api-complete.test.ts,
notification-settings.test.ts, and users-keys-complete.test.ts to import and use
callApi and the shared beforeAll/login helper instead of duplicating the logic
in each file.

In `@tests/e2e/users-keys-complete.test.ts`:
- Line 37: Move the module-level beforeAll so it appears before the module-level
afterAll (make the order match other tests) and rename the inconsistent
authToken identifier to sessionToken: update the callApi function
signature/default to use sessionToken and replace all authToken uses with
sessionToken, ensuring the module variable let sessionToken: string | undefined;
is the single source of truth and that afterAll (cleanup) stays after the
now-earlier beforeAll setup.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

13 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

const shouldRetry = response.status === 503 && errorCode === "SESSION_CREATE_FAILED";
if (!shouldRetry || attempt >= maxAttempts) {
throw new Error(`[e2e] login failed: ${response.status} ${text}`);
}
Copy link

Choose a reason for hiding this comment

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

Exponential backoff grows too slowly - 100 * 2^(attempt-1) reaches only 512ms at attempt 4. Since sleep caps at 1000ms, attempts 4-10 all wait the same 1s. Use 200 * 2^(attempt-1) or increase the cap.

Suggested change
}
await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 62

Comment:
Exponential backoff grows too slowly - `100 * 2^(attempt-1)` reaches only 512ms at attempt 4. Since `sleep` caps at 1000ms, attempts 4-10 all wait the same 1s. Use `200 * 2^(attempt-1)` or increase the cap.

```suggestion
        await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

if (attempt >= maxAttempts) {
break;
}

Copy link

Choose a reason for hiding this comment

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

Same backoff issue - grows too slowly with 100ms base. Also, this retry is for non-503 errors which seems overly aggressive.

Suggested change
await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 83

Comment:
Same backoff issue - grows too slowly with 100ms base. Also, this retry is for non-503 errors which seems overly aggressive.

```suggestion
      await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
```

How can I resolve this? If you propose a fix, please make it concise.

disabled={isPending}
tabStatus={getTabStatus()}
/>
<div className="order-2 md:order-1 shrink-0">
Copy link

Choose a reason for hiding this comment

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

Verify that the submit button (likely in a footer component) is accessible on mobile now that nav is repositioned below content.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 556

Comment:
Verify that the submit button (likely in a footer component) is accessible on mobile now that nav is repositioned below content.

How can I resolve this? If you propose a fix, please make it concise.

maxConcurrency: 5, // 最大并发测试数
pool: "threads", // 使用线程池(推荐)
// 高核机器/Windows 下 threads worker 过多可能触发 EMFILE / 资源争用导致用例超时。
// 允许通过环境变量覆盖:VITEST_MAX_WORKERS=...
Copy link

Choose a reason for hiding this comment

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

Default of 8 workers might be too conservative for modern machines - consider Math.min(os.cpus().length, 16) or a higher default (12-16) for better performance on multi-core systems.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: vitest.config.ts
Line: 100

Comment:
Default of 8 workers might be too conservative for modern machines - consider `Math.min(os.cpus().length, 16)` or a higher default (12-16) for better performance on multi-core systems.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/e2e/_helpers/auth.ts`:
- Around line 59-85: The thrown error inside the try when !shouldRetry is being
caught by the outer catch and causes unintended retries; update the login loop
so non-retriable failures are rethrown immediately instead of being swallowed:
either move the throw for the !shouldRetry branch out of the try (so it escapes
the loop) or tag the thrown Error (e.g., add a nonRetriable flag) and in the
catch check that flag (using shouldRetry/attempt/maxAttempts) and rethrow
immediately; adjust handling of lastError/sleep accordingly so only truly
retriable 503/SESSION_CREATE_FAILED paths are retried.

---

Duplicate comments:
In `@tests/e2e/_helpers/auth.ts`:
- Around line 24-27: The extractCookieValue function uses new RegExp with a
concatenated cookieName which opens a ReDoS/unsafe-regex risk; replace the regex
approach in extractCookieValue with deterministic string parsing: trim the
header, split it on ';' into cookie segments, iterate segments and for each trim
and check startsWith(`${cookieName}=`) (or use indexOf) to extract and return
the substring after '=' (or null if not found). Ensure you only perform plain
string operations (no RegExp) and preserve existing return semantics (string |
null).

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

14 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 267 to 271
<div className="absolute top-0 left-0 right-0 h-0.5 bg-muted">
<motion.div
className="h-full bg-primary"
initial={{ width: "20%" }}
animate={{
width: `${((TAB_CONFIG.findIndex((t) => t.id === activeTab) + 1) / TAB_CONFIG.length) * 100}%`,
}}
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
Copy link

Choose a reason for hiding this comment

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

animation now disabled - initial and animate both set to stepProgressWidth, causing no visual transition when tabs change

Suggested change
<div className="absolute top-0 left-0 right-0 h-0.5 bg-muted">
<motion.div
className="h-full bg-primary"
initial={{ width: "20%" }}
animate={{
width: `${((TAB_CONFIG.findIndex((t) => t.id === activeTab) + 1) / TAB_CONFIG.length) * 100}%`,
}}
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
initial={{ width: 0 }}
animate={{ width: stepProgressWidth }}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
Line: 267-271

Comment:
animation now disabled - `initial` and `animate` both set to `stepProgressWidth`, causing no visual transition when tabs change

```suggestion
            initial={{ width: 0 }}
            animate={{ width: stepProgressWidth }}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +77 to 79
// Skip Redis connection during Next.js production build phase (avoid connection attempts)
if (process.env.NEXT_PHASE === "phase-production-build") {
return null;
Copy link

Choose a reason for hiding this comment

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

removing CI === "true" check allows Redis connections during CI, but the comment only mentions Next.js build phase - verify CI environment can handle Redis connections or update logic

if CI should still skip Redis (but E2E needs it), consider:

Suggested change
// Skip Redis connection during Next.js production build phase (avoid connection attempts)
if (process.env.NEXT_PHASE === "phase-production-build") {
return null;
// Skip Redis during Next.js build, but allow in CI for E2E tests
if (process.env.NEXT_PHASE === "phase-production-build") {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/client.ts
Line: 77-79

Comment:
removing `CI === "true"` check allows Redis connections during CI, but the comment only mentions Next.js build phase - verify CI environment can handle Redis connections or update logic

if CI should still skip Redis (but E2E needs it), consider:
```suggestion
  // Skip Redis during Next.js build, but allow in CI for E2E tests
  if (process.env.NEXT_PHASE === "phase-production-build") {
```

How can I resolve this? If you propose a fix, please make it concise.

throw new Error(`[e2e] login failed: ${response.status} ${text}`);
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
Copy link

Choose a reason for hiding this comment

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

slow exponential backoff - 100 * 2^(attempt-1) only reaches 512ms at attempt 4, then Math.min(1000, ...) caps remaining attempts at 1s. First 4 attempts happen within ~1.5s total

Suggested change
await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 64

Comment:
slow exponential backoff - `100 * 2^(attempt-1)` only reaches 512ms at attempt 4, then `Math.min(1000, ...)` caps remaining attempts at 1s. First 4 attempts happen within ~1.5s total

```suggestion
        await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

break;
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
Copy link

Choose a reason for hiding this comment

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

retry logic applies to all errors (network, parse failures, etc), not just transient server issues - may mask bugs in test setup

Suggested change
await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 84

Comment:
retry logic applies to all errors (network, parse failures, etc), not just transient server issues - may mask bugs in test setup

```suggestion
      await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

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

🧹 Nitpick comments (1)
src/lib/redis/client.ts (1)

94-96: 建议:连接永久断开后重置单例,避免返回僵尸客户端

retryStrategy 返回非数字值时,ioredis 将停止重连,"the connection will be lost forever unless the user calls redis.connect() manually"。

当前实现中,retryStrategy 在第 5 次后返回 null(第 108 行),但模块级 redisClient 单例并未被清空。此后所有 getRedisClient() 调用都会在第 94-96 行命中缓存并返回这个不可用的死客户端,而不会尝试重新初始化。

现在 CI 环境也会建立真实连接,该问题在 CI 流水线中更容易被触发(例如 Redis 服务启动滞后时)。

♻️ 建议:监听 `end` 事件时重置单例
     redisClient.on("close", () => {
       logger.warn("[Redis] Connection closed", { redisUrl: safeRedisUrl });
     });
+
+    redisClient.on("end", () => {
+      logger.warn("[Redis] Connection permanently closed, resetting client", { redisUrl: safeRedisUrl });
+      redisClient = null;
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/redis/client.ts` around lines 94 - 96, The module-level singleton
redisClient can become a "dead" client when retryStrategy returns null (stops
reconnection), so update the Redis client initialization (the code that creates
redisClient and defines retryStrategy) to attach an 'end' (and optionally
'close') event handler that sets the module-level redisClient = null so
subsequent getRedisClient() calls will reinitialize a new client; also consider
checking client status in getRedisClient() (e.g., not just truthy but
connected/ready) before returning the cached instance to avoid returning a
zombie client.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/redis/client.ts`:
- Around line 94-96: The module-level singleton redisClient can become a "dead"
client when retryStrategy returns null (stops reconnection), so update the Redis
client initialization (the code that creates redisClient and defines
retryStrategy) to attach an 'end' (and optionally 'close') event handler that
sets the module-level redisClient = null so subsequent getRedisClient() calls
will reinitialize a new client; also consider checking client status in
getRedisClient() (e.g., not just truthy but connected/ready) before returning
the cached instance to avoid returning a zombie client.

@tesgth032
Copy link
Contributor Author

已按 CodeRabbit 建议补齐并修正:

  • 增加 safe-area-bottom 工具类(iOS 安全区)
  • FormTabNav 进度条初始宽度改为动态计算,避免 hardcode 20% 与变量遮蔽

另外修复了 PR 中 API E2E Tests 的失败:

  • E2E 用例通过 /api/auth/login 登录换取会话 �uth-token(兼容 SESSION_TOKEN_MODE=opaque)
  • 登录过程对 SESSION_CREATE_FAILED 做了短暂重试,等待 Redis 就绪
  • 允许 CI 环境在非 build 阶段连接 Redis,避免 opaque session 无法创建

当前 Actions:build/lint/typecheck/unit/integration/api tests 均已通过。

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

19 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 151 to 169
@supports (height: 100dvh) {
.max-h-\[50vh\] {
max-height: 50dvh;
}
.max-h-\[70vh\] {
max-height: 70dvh;
}
.max-h-\[80vh\] {
max-height: 80dvh;
}
.max-h-\[85vh\] {
max-height: 85dvh;
}
.max-h-\[90vh\] {
max-height: 90dvh;
}
.max-h-\[95vh\] {
max-height: 95dvh;
}
Copy link

Choose a reason for hiding this comment

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

these CSS overrides using @supports (height: 100dvh) will only apply to browsers that support dvh, but the classes like max-h-[85vh] will still use vh in browsers that don't support dvh

the --cch-viewport-height CSS variable approach used elsewhere in the PR (lines 48, 82-86) is more consistent - it provides a single source of truth that falls back from dvh to vh

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/globals.css
Line: 151-169

Comment:
these CSS overrides using `@supports (height: 100dvh)` will only apply to browsers that support `dvh`, but the classes like `max-h-[85vh]` will still use `vh` in browsers that don't support `dvh`

the `--cch-viewport-height` CSS variable approach used elsewhere in the PR (lines 48, 82-86) is more consistent - it provides a single source of truth that falls back from `dvh` to `vh`

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@@ -553,18 +553,20 @@ function ProviderFormContent({
<form onSubmit={handleSubmit} className="flex flex-col h-full max-h-[85vh]">
Copy link

Choose a reason for hiding this comment

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

form still uses max-h-[85vh] which will be overridden to 85dvh by the CSS in globals.css - but this creates inconsistency with other viewport calculations that use the --cch-viewport-height variable

Suggested change
<form onSubmit={handleSubmit} className="flex flex-col h-full max-h-[85vh]">
<form onSubmit={handleSubmit} className="flex flex-col h-full max-h-[calc(var(--cch-viewport-height,100vh)*0.85)]">
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 553

Comment:
form still uses `max-h-[85vh]` which will be overridden to `85dvh` by the CSS in `globals.css` - but this creates inconsistency with other viewport calculations that use the `--cch-viewport-height` variable

```suggestion
    <form onSubmit={handleSubmit} className="flex flex-col h-full max-h-[calc(var(--cch-viewport-height,100vh)*0.85)]">
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +556 to +564
<div className="order-2 md:order-1 shrink-0">
{/* Tab Navigation */}
<FormTabNav
activeTab={state.ui.activeTab}
onTabChange={handleTabChange}
disabled={isPending}
tabStatus={getTabStatus()}
/>
</div>
Copy link

Choose a reason for hiding this comment

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

wrapping FormTabNav in a div with shrink-0 creates an extra DOM layer - the wrapper div prevents FormTabNav from being a direct flex child, which may cause layout issues if FormTabNav has its own flex/sizing logic

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
Line: 556-564

Comment:
wrapping `FormTabNav` in a `div` with `shrink-0` creates an extra DOM layer - the wrapper div prevents `FormTabNav` from being a direct flex child, which may cause layout issues if `FormTabNav` has its own flex/sizing logic

How can I resolve this? If you propose a fix, please make it concise.

throw new Error(`[e2e] login failed: ${response.status} ${text}`);
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
Copy link

Choose a reason for hiding this comment

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

exponential backoff base (100ms) is too small - with cap at 1000ms, attempts reach max delay quickly: 100ms, 200ms, 400ms, 800ms, then 1000ms for attempts 5-10

Suggested change
await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 64

Comment:
exponential backoff base (100ms) is too small - with cap at 1000ms, attempts reach max delay quickly: 100ms, 200ms, 400ms, 800ms, then 1000ms for attempts 5-10

```suggestion
        await sleep(Math.min(2000, 200 * 2 ** (attempt - 1)));
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 78 to 85
} catch (error) {
lastError = error;
if (attempt >= maxAttempts) {
break;
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
}
Copy link

Choose a reason for hiding this comment

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

catch block retries for ALL errors (network failures, parse errors, etc), not just transient server issues - this masks legitimate failures and causes tests to hang for extended periods

the retry should only happen for specific retryable errors, or this block should be removed entirely since the non-ok response case above already handles 503 retries

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 78-85

Comment:
catch block retries for ALL errors (network failures, parse errors, etc), not just transient server issues - this masks legitimate failures and causes tests to hang for extended periods

the retry should only happen for specific retryable errors, or this block should be removed entirely since the non-ok response case above already handles 503 retries

How can I resolve this? If you propose a fix, please make it concise.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

81 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +18 to +21
return combined
.split(/,(?=[^;]+?=)/g)
.map((s) => s.trim())
.filter(Boolean);
Copy link

Choose a reason for hiding this comment

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

regex /,(?=[^;]+?=)/g can incorrectly split Set-Cookie headers containing RFC-compliant date values like Expires=Wed, 21 Oct 2026 07:28:00 GMT

the lookahead (?=[^;]+?=) prevents splitting at commas within individual cookie attributes, but dates like Expires=Wed, 21 Oct 2026 still match because the pattern sees 21 followed by space and other text, then eventually finds = in GMT or subsequent attributes

if the server sends Expires with RFC 1123 dates, this will break cookie parsing

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 18-21

Comment:
regex `/,(?=[^;]+?=)/g` can incorrectly split `Set-Cookie` headers containing RFC-compliant date values like `Expires=Wed, 21 Oct 2026 07:28:00 GMT`

the lookahead `(?=[^;]+?=)` prevents splitting at commas within individual cookie attributes, but dates like `Expires=Wed, 21 Oct 2026` still match because the pattern sees `21` followed by space and other text, then eventually finds `=` in `GMT` or subsequent attributes

if the server sends `Expires` with RFC 1123 dates, this will break cookie parsing

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +59 to +67
});
} catch (error) {
lastError = error;
if (attempt >= maxAttempts) {
break;
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
continue;
Copy link

Choose a reason for hiding this comment

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

retry on ALL fetch errors (network, DNS, timeouts) masks legitimate test setup failures - if API_BASE_URL is misconfigured or network is down, tests hang for ~6 seconds (10 attempts × ~600ms avg) instead of failing fast

only retry transient errors like ECONNRESET / ETIMEDOUT, or remove this catch block entirely since line 82-93 already handles HTTP 503 retries

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 59-67

Comment:
retry on ALL fetch errors (network, DNS, timeouts) masks legitimate test setup failures - if `API_BASE_URL` is misconfigured or network is down, tests hang for ~6 seconds (10 attempts × ~600ms avg) instead of failing fast

only retry transient errors like `ECONNRESET` / `ETIMEDOUT`, or remove this catch block entirely since line 82-93 already handles HTTP 503 retries

How can I resolve this? If you propose a fix, please make it concise.

if (attempt >= maxAttempts) {
break;
}

Copy link

Choose a reason for hiding this comment

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

exponential backoff base is too small - with 100 * 2^(attempt-1) and cap at 1000ms, delays are: 100ms, 200ms, 400ms, 800ms, then 1000ms for attempts 5-10

attempts reach max delay very quickly, meaning attempts 5-10 all wait the same 1 second - consider using a larger base like 200ms or increasing the cap

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 65

Comment:
exponential backoff base is too small - with `100 * 2^(attempt-1)` and cap at 1000ms, delays are: 100ms, 200ms, 400ms, 800ms, then 1000ms for attempts 5-10

attempts reach max delay very quickly, meaning attempts 5-10 all wait the same 1 second - consider using a larger base like 200ms or increasing the cap

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +277 to +278
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
Copy link

Choose a reason for hiding this comment

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

both initial and animate set to same value stepProgressWidth disables the animation - no visual transition when tabs change

remove initial prop to animate from current position

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
Line: 277-278

Comment:
both `initial` and `animate` set to same value `stepProgressWidth` disables the animation - no visual transition when tabs change

remove `initial` prop to animate from current position

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@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: 1

♻️ Duplicate comments (1)
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (1)

76-82: useEffect 双向同步——已正确修复

当前代码 setClientRestrictionsEnabled(hasAnyClientRestrictions) 同时处理了 false→truetrue→false 两个方向,与之前审查建议一致。

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

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
around lines 76 - 82, The two-way sync using hasAnyClientRestrictions with
clientRestrictionsEnabled is correct—keep the initial state const
[clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions) and the useEffect that calls
setClientRestrictionsEnabled(hasAnyClientRestrictions) with
[hasAnyClientRestrictions] as the dependency; do not add additional syncing
logic or duplicate effect handlers, and remove any accidental duplicate comments
or redundant code related to this synchronization.
🧹 Nitpick comments (3)
src/components/form/client-restrictions-editor.tsx (2)

64-74: TagInput 缺少 onInvalidTag 回调——标签被静默拒绝时用户无任何反馈

第 65-73 行的 TagInput 设置了 maxTagLength={64}maxTags={50},但未传入 onInvalidTag。当 TagInput 内部因超长、超数量或重复等原因拒绝标签时,用户不会收到任何错误提示——与 routing-section.tsx 中通过 onInvalidTag + toast.error 提供反馈的做法不一致。

♻️ 建议补充 onInvalidTag 支持

ClientRestrictionListEditorProps 中增加可选回调:

 interface ClientRestrictionListEditorProps {
   label: string;
   values: string[];
   placeholder?: string;
   disabled?: boolean;
   getPresetLabel: (presetValue: string) => string;
   onTogglePreset: (presetValue: string, checked: boolean) => void;
   onCustomChange: (customValues: string[]) => void;
+  onInvalidTag?: (tag: string, reason: string) => void;
   className?: string;
 }
-function ClientRestrictionListEditor({
+function ClientRestrictionListEditor({
   label,
   values,
   placeholder,
   disabled,
   getPresetLabel,
   onTogglePreset,
   onCustomChange,
+  onInvalidTag,
   className,
 }: ClientRestrictionListEditorProps) {
         <TagInput
           value={customValues}
           onChange={onCustomChange}
           placeholder={placeholder}
           maxTagLength={64}
           maxTags={50}
           disabled={disabled}
           validateTag={() => true}
+          onInvalidTag={onInvalidTag}
         />

ClientRestrictionsEditorProps 中同样可以暴露该字段,或由消费方(routing-section.tsx)传入 onInvalidTag 并向下透传,以保持与其他 TagInput 用法的一致性。

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

In `@src/components/form/client-restrictions-editor.tsx` around lines 64 - 74, The
TagInput here is missing an onInvalidTag handler so rejected tags (too long, too
many, duplicates) give no feedback; update the props interfaces
ClientRestrictionListEditorProps and ClientRestrictionsEditorProps to include an
optional onInvalidTag?: (reason: string) => void, expose/accept that prop in the
component that renders TagInput, and pass it down to TagInput alongside
value={customValues} and onChange={onCustomChange}; consumers (e.g.,
routing-section.tsx) can then provide an onInvalidTag that calls toast.error to
show the user feedback.

126-144: handleAllowedCustomChange / handleBlockedCustomChange 中存在冗余的规范化计算

mergePresetAndCustomClients(blocked, splitPresetAndCustomClients(blocked).customValues) 等价于 blocked 本身(在数据已规范化的前提下),实际上只是去重。若 blocked / allowed 在写入时始终经过 mergePresetAndCustomClientstogglePresetSelection 维护,可直接用 new Set(...) 替代:

♻️ 建议简化
 const handleAllowedCustomChange = (customValues: string[]) => {
-  const blockedMerged = mergePresetAndCustomClients(
-    blocked,
-    splitPresetAndCustomClients(blocked).customValues
-  );
-  const blockedValueSet = new Set(blockedMerged);
+  const blockedValueSet = new Set(blocked);
   const filteredCustomValues = customValues.filter((value) => !blockedValueSet.has(value));
   onAllowedChange(mergePresetAndCustomClients(allowed, filteredCustomValues));
 };

 const handleBlockedCustomChange = (customValues: string[]) => {
-  const allowedMerged = mergePresetAndCustomClients(
-    allowed,
-    splitPresetAndCustomClients(allowed).customValues
-  );
-  const allowedValueSet = new Set(allowedMerged);
+  const allowedValueSet = new Set(allowed);
   const filteredCustomValues = customValues.filter((value) => !allowedValueSet.has(value));
   onBlockedChange(mergePresetAndCustomClients(blocked, filteredCustomValues));
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/form/client-restrictions-editor.tsx` around lines 126 - 144,
The handlers perform redundant normalization by calling
mergePresetAndCustomClients(..., splitPresetAndCustomClients(...).customValues)
even though allowed/blocked are already normalized; replace those expressions
with a simple Set created from the current allowed or blocked arrays (e.g.,
const blockedValueSet = new Set(blocked) / const allowedValueSet = new
Set(allowed)), use that set to filter the incoming customValues, and then call
onAllowedChange(mergePresetAndCustomClients(allowed, filteredCustomValues)) or
onBlockedChange(mergePresetAndCustomClients(blocked, filteredCustomValues))
respectively to produce the final normalized value; update
handleAllowedCustomChange and handleBlockedCustomChange accordingly and remove
the redundant split/merge calls.
tests/unit/lib/endpoint-circuit-breaker.test.ts (1)

490-493: 建议在 waitForMockCalled 前显式调用 vi.useRealTimers(),与其他用例保持一致

该测试未启用 Fake Timers,因此第 492 行的 waitForMockCalled 当前可正常运行。但与第 125–126 行、第 453–454 行的模式不一致——那两处均在调用前先显式恢复真实计时器。若将来此测试需要用 vi.useFakeTimers() 冻结 Date.now(),遗漏该调用将直接导致无限挂起(见上方第 28–33 行的说明)。

♻️ 建议修改
     expect(getEndpointCircuitStateSync(200)).toBe("open");
+
+    // 与其他用例保持一致:确保真实计时器生效后再轮询告警。
+    vi.useRealTimers();
     await waitForMockCalled(sendAlertMock);
     expect(sendAlertMock).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/lib/endpoint-circuit-breaker.test.ts` around lines 490 - 493,
Insert a call to vi.useRealTimers() immediately before the await
waitForMockCalled(sendAlertMock) call in the test so timers are restored
(matching the pattern used elsewhere); specifically, update the test surrounding
waitForMockCalled and sendAlertMock to call vi.useRealTimers() first to avoid
hanging if fake timers are enabled in other tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/lib/endpoint-circuit-breaker.test.ts`:
- Around line 28-33: The helper waitForMockCalled currently silent-exits on
timeout and can hang under fake timers; update waitForMockCalled to (1) throw a
clear Error like "timeout waiting for mock" when the timeout elapses instead of
returning void, and (2) guard against fake timers by temporarily switching to
real timers (save current timer mode, call vi.useRealTimers(), then restore the
original timer mode after await loop) so Date.now and setTimeout advance;
reference the waitForMockCalled function and use
vi.useRealTimers()/vi.useFakeTimers() (or equivalent restore) around the polling
loop to ensure it never deadlocks under fake timers.

---

Duplicate comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx:
- Around line 76-82: The two-way sync using hasAnyClientRestrictions with
clientRestrictionsEnabled is correct—keep the initial state const
[clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions) and the useEffect that calls
setClientRestrictionsEnabled(hasAnyClientRestrictions) with
[hasAnyClientRestrictions] as the dependency; do not add additional syncing
logic or duplicate effect handlers, and remove any accidental duplicate comments
or redundant code related to this synchronization.

---

Nitpick comments:
In `@src/components/form/client-restrictions-editor.tsx`:
- Around line 64-74: The TagInput here is missing an onInvalidTag handler so
rejected tags (too long, too many, duplicates) give no feedback; update the
props interfaces ClientRestrictionListEditorProps and
ClientRestrictionsEditorProps to include an optional onInvalidTag?: (reason:
string) => void, expose/accept that prop in the component that renders TagInput,
and pass it down to TagInput alongside value={customValues} and
onChange={onCustomChange}; consumers (e.g., routing-section.tsx) can then
provide an onInvalidTag that calls toast.error to show the user feedback.
- Around line 126-144: The handlers perform redundant normalization by calling
mergePresetAndCustomClients(..., splitPresetAndCustomClients(...).customValues)
even though allowed/blocked are already normalized; replace those expressions
with a simple Set created from the current allowed or blocked arrays (e.g.,
const blockedValueSet = new Set(blocked) / const allowedValueSet = new
Set(allowed)), use that set to filter the incoming customValues, and then call
onAllowedChange(mergePresetAndCustomClients(allowed, filteredCustomValues)) or
onBlockedChange(mergePresetAndCustomClients(blocked, filteredCustomValues))
respectively to produce the final normalized value; update
handleAllowedCustomChange and handleBlockedCustomChange accordingly and remove
the redundant split/merge calls.

In `@tests/unit/lib/endpoint-circuit-breaker.test.ts`:
- Around line 490-493: Insert a call to vi.useRealTimers() immediately before
the await waitForMockCalled(sendAlertMock) call in the test so timers are
restored (matching the pattern used elsewhere); specifically, update the test
surrounding waitForMockCalled and sendAlertMock to call vi.useRealTimers() first
to avoid hanging if fake timers are enabled in other tests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6265bdc and cc61d09.

📒 Files selected for processing (4)
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  • src/app/global-error.tsx
  • src/components/form/client-restrictions-editor.tsx
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/global-error.tsx

@tesgth032
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

81 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 131 to 139
const handleAllowedCustomChange = (customValues: string[]) => {
const blockedMerged = mergePresetAndCustomClients(
blocked,
splitPresetAndCustomClients(blocked).customValues
);
const blockedValueSet = new Set(blockedMerged);
const filteredCustomValues = customValues.filter((value) => !blockedValueSet.has(value));
onAllowedChange(mergePresetAndCustomClients(allowed, filteredCustomValues));
};
Copy link

Choose a reason for hiding this comment

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

Inefficient re-splitting of already-known preset values

On line 132-135, splitPresetAndCustomClients(blocked) is called to get custom values, but this is redundant — the caller (routing-section.tsx) already maintains the full blocked array. The function then merges presets back in at line 138 using mergePresetAndCustomClients(allowed, filteredCustomValues), which re-splits internally.

This creates unnecessary work: split → filter → merge (which splits again). Consider passing the current preset selections separately or restructuring to avoid multiple split/merge cycles.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/form/client-restrictions-editor.tsx
Line: 131-139

Comment:
**Inefficient re-splitting of already-known preset values**

On line 132-135, `splitPresetAndCustomClients(blocked)` is called to get custom values, but this is redundant — the caller (`routing-section.tsx`) already maintains the full `blocked` array. The function then merges presets back in at line 138 using `mergePresetAndCustomClients(allowed, filteredCustomValues)`, which re-splits internally.

This creates unnecessary work: split → filter → merge (which splits again). Consider passing the current preset selections separately or restructuring to avoid multiple split/merge cycles.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +77 to +82
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);

useEffect(() => {
setClientRestrictionsEnabled(hasAnyClientRestrictions);
}, [hasAnyClientRestrictions]);
Copy link

Choose a reason for hiding this comment

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

useState initializer runs on every render

useState(hasAnyClientRestrictions) on line 77-78 computes the initial value on every render (since hasAnyClientRestrictions is recalculated each time), even though useState only uses it on mount. Use a function initializer to avoid unnecessary computation:

Suggested change
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);
useEffect(() => {
setClientRestrictionsEnabled(hasAnyClientRestrictions);
}, [hasAnyClientRestrictions]);
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() =>
allowedClients.length > 0 || blockedClients.length > 0
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Line: 77-82

Comment:
**useState initializer runs on every render**

`useState(hasAnyClientRestrictions)` on line 77-78 computes the initial value on every render (since `hasAnyClientRestrictions` is recalculated each time), even though `useState` only uses it on mount. Use a function initializer to avoid unnecessary computation:

```suggestion
  const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() => 
    allowedClients.length > 0 || blockedClients.length > 0
  );
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +80 to +82
useEffect(() => {
setClientRestrictionsEnabled(hasAnyClientRestrictions);
}, [hasAnyClientRestrictions]);
Copy link

Choose a reason for hiding this comment

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

useEffect creates infinite update risk if parent re-adds restrictions

When clientRestrictionsEnabled is true and user edits the lists externally (e.g., via form reset or batch operations that directly modify allowedClients/blockedClients to non-empty arrays), this effect will re-run and call setClientRestrictionsEnabled(true) again. While React will skip the state update if the value is already true, this creates a dependency on state that the component itself controls, which can cause subtle bugs.

The real concern: if external code clears both lists (allowedClients = [], blockedClients = []) while the toggle is still enabled, the effect will disable the toggle, but the user's intent (to keep it enabled and add new restrictions) is lost.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Line: 80-82

Comment:
**useEffect creates infinite update risk if parent re-adds restrictions**

When `clientRestrictionsEnabled` is `true` and user edits the lists externally (e.g., via form reset or batch operations that directly modify `allowedClients`/`blockedClients` to non-empty arrays), this effect will re-run and call `setClientRestrictionsEnabled(true)` again. While React will skip the state update if the value is already `true`, this creates a dependency on state that the component itself controls, which can cause subtle bugs.

The real concern: if external code clears both lists (`allowedClients = []`, `blockedClients = []`) while the toggle is still enabled, the effect will disable the toggle, but the user's intent (to keep it enabled and add new restrictions) is lost.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +99 to 104
if (redisClient.status === "end") {
redisClient = null;
} else {
return redisClient;
}
}
Copy link

Choose a reason for hiding this comment

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

Race condition: client.status check may not be reliable

If redisClient.status === "end" on line 99, you reset redisClient = null and fall through to create a new client. However, between the status check and the new client creation, the old client might not be fully cleaned up. ioredis maintains internal state that may not be immediately garbage-collected.

Consider explicitly calling disconnect() on the ended client before nulling it:

Suggested change
if (redisClient.status === "end") {
redisClient = null;
} else {
return redisClient;
}
}
if (redisClient) {
if (redisClient.status === "end") {
redisClient.disconnect(false);
redisClient = null;
} else {
return redisClient;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/client.ts
Line: 99-104

Comment:
**Race condition: client.status check may not be reliable**

If `redisClient.status === "end"` on line 99, you reset `redisClient = null` and fall through to create a new client. However, between the status check and the new client creation, the old client might not be fully cleaned up. ioredis maintains internal state that may not be immediately garbage-collected.

Consider explicitly calling `disconnect()` on the ended client before nulling it:

```suggestion
  if (redisClient) {
    if (redisClient.status === "end") {
      redisClient.disconnect(false);
      redisClient = null;
    } else {
      return redisClient;
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +142 to +146
client.on("end", () => {
if (redisClient !== client) return;
logger.warn("[Redis] Connection ended, resetting client", { redisUrl: safeRedisUrl });
redisClient = null;
});
Copy link

Choose a reason for hiding this comment

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

double-null assignment between end handler and closeRedis — when quit() fires, this handler sets redisClient = null at line 145, then closeRedis's finally block also tries to set it at line 170 (but the guard will be false)

safe but confusing — add a comment explaining the interaction

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/client.ts
Line: 142-146

Comment:
double-null assignment between `end` handler and `closeRedis` — when `quit()` fires, this handler sets `redisClient = null` at line 145, then `closeRedis`'s finally block also tries to set it at line 170 (but the guard will be false)

safe but confusing — add a comment explaining the interaction

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx (1)

80-82: 双向同步逻辑已正确实现

setClientRestrictionsEnabled(hasAnyClientRestrictions) 已涵盖 true→falsefalse→true 两个方向,先前提出的问题已修复。

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

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
around lines 80 - 82, The effect updating client restriction state is already
correctly implemented; keep the useEffect that calls
setClientRestrictionsEnabled(hasAnyClientRestrictions) with
hasAnyClientRestrictions in the dependency array (in routing-section.tsx), since
this covers both true→false and false→true transitions—no code changes required
for useEffect, use setClientRestrictionsEnabled and hasAnyClientRestrictions
as-is.
🧹 Nitpick comments (1)
src/components/form/client-restrictions-editor.tsx (1)

131-149: handleAllowedCustomChangehandleBlockedCustomChange 中的冗余运算

mergePresetAndCustomClients(X, splitPresetAndCustomClients(X).customValues)X 拆分后再原样合并,结果等价于 X 本身,因此 new Set(blockedMerged)new Set(blocked)handleBlockedCustomChange 同理)。建议简化:

优化建议
  const handleAllowedCustomChange = (customValues: string[]) => {
-   const blockedMerged = mergePresetAndCustomClients(
-     blocked,
-     splitPresetAndCustomClients(blocked).customValues,
-   );
-   const blockedValueSet = new Set(blockedMerged);
+   const blockedValueSet = new Set(blocked);
    const filteredCustomValues = customValues.filter((value) => !blockedValueSet.has(value));
    onAllowedChange(mergePresetAndCustomClients(allowed, filteredCustomValues));
  };

  const handleBlockedCustomChange = (customValues: string[]) => {
-   const allowedMerged = mergePresetAndCustomClients(
-     allowed,
-     splitPresetAndCustomClients(allowed).customValues,
-   );
-   const allowedValueSet = new Set(allowedMerged);
+   const allowedValueSet = new Set(allowed);
    const filteredCustomValues = customValues.filter((value) => !allowedValueSet.has(value));
    onBlockedChange(mergePresetAndCustomClients(blocked, filteredCustomValues));
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/form/client-restrictions-editor.tsx` around lines 131 - 149,
The two handlers perform redundant split/merge operations that return the
original list; simplify handleAllowedCustomChange and handleBlockedCustomChange
by using the original arrays when building the exclusion Set (use new
Set(blocked) in handleAllowedCustomChange and new Set(allowed) in
handleBlockedCustomChange) and keep the rest of the logic (filtering
customValues and calling onAllowedChange/onBlockedChange with
mergePresetAndCustomClients(..., filteredCustomValues)) unchanged; update only
the Set construction to remove the unnecessary calls to
mergePresetAndCustomClients(... splitPresetAndCustomClients(...).customValues).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx:
- Around line 80-82: The effect updating client restriction state is already
correctly implemented; keep the useEffect that calls
setClientRestrictionsEnabled(hasAnyClientRestrictions) with
hasAnyClientRestrictions in the dependency array (in routing-section.tsx), since
this covers both true→false and false→true transitions—no code changes required
for useEffect, use setClientRestrictionsEnabled and hasAnyClientRestrictions
as-is.

---

Nitpick comments:
In `@src/components/form/client-restrictions-editor.tsx`:
- Around line 131-149: The two handlers perform redundant split/merge operations
that return the original list; simplify handleAllowedCustomChange and
handleBlockedCustomChange by using the original arrays when building the
exclusion Set (use new Set(blocked) in handleAllowedCustomChange and new
Set(allowed) in handleBlockedCustomChange) and keep the rest of the logic
(filtering customValues and calling onAllowedChange/onBlockedChange with
mergePresetAndCustomClients(..., filteredCustomValues)) unchanged; update only
the Set construction to remove the unnecessary calls to
mergePresetAndCustomClients(... splitPresetAndCustomClients(...).customValues).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between cc61d09 and a98ec3a.

📒 Files selected for processing (3)
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  • src/components/form/client-restrictions-editor.tsx
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/lib/endpoint-circuit-breaker.test.ts

@tesgth032
Copy link
Contributor Author

本次补充优化(commit 5a1290d):

  • 客户端限制编辑器:改为“白名单/黑名单”两列 TagInput,支持下拉建议选择预设客户端 + 直接输入自定义 UA/模式;并在输入不合法(空/重复/过长/超量)时用 toast 给出反馈。
  • 允许的模型:将选择框 + 摘要信息做成同一组,缩短摘要与输入框的距离,避免与下一项标题过近导致层级误读。
  • 模型选择下拉:PopoverContent 增加 max-w-[calc(100vw-2rem)],小屏不再溢出。

验证:

  • bun run typecheck
  • bun run lint
  • bun run test
  • bun run build

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

82 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +77 to +78
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);
Copy link

Choose a reason for hiding this comment

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

hasAnyClientRestrictions computed on every render but only used during mount

Suggested change
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() =>
allowedClients.length > 0 || blockedClients.length > 0
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Line: 77-78

Comment:
`hasAnyClientRestrictions` computed on every render but only used during mount

```suggestion
  const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() =>
    allowedClients.length > 0 || blockedClients.length > 0
  );
```

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032
Copy link
Contributor Author

@coderabbitai review

请基于最新提交复审。如果没有阻塞性问题,麻烦给出 APPROVED 以清除历史 CHANGES_REQUESTED。

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

@tesgth032: 收到!我将基于最新提交(5a1290d1)进行复审。

您在本次提交中完成的优化非常棒:

客户端限制编辑器:白名单/黑名单双列布局,支持预设建议与自定义输入,并提供实时验证反馈
模型选择 UI:将选择框与摘要信息分组,优化视觉层级
小屏适配:PopoverContent 增加最大宽度约束,避免溢出

您已完成所有必要的验证(typecheck/lint/test/build),并且持续按照审查建议迭代优化。整个 PR 的移动端适配方案(CSS 变量、safe-area、dvh fallback)和稳健性改进(Redis 生命周期、E2E 认证)都已就绪。

没有发现阻塞性问题,我现在将批准此 PR 以清除之前的 CHANGES_REQUESTED 状态。


🧠 Learnings used
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
✅ Actions performed

Comments resolved and changes approved.


Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

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

🧹 Nitpick comments (3)
src/app/[locale]/settings/providers/_components/model-multi-select.tsx (2)

61-79: 建议用 useMemo 替代 IIFE 计算 displayedModels

当前写法在每次渲染时都执行 Set 构建与两次数组遍历。改用 useMemo 可在 availableModelsselectedModels 未变化时跳过重新计算,在模型列表较大时尤为明显。

♻️ 改用 useMemo
-  const displayedModels = (() => {
-    const seen = new Set<string>();
-    const merged: string[] = [];
-
-    for (const model of availableModels) {
-      if (seen.has(model)) continue;
-      seen.add(model);
-      merged.push(model);
-    }
-
-    // 关键:把已选中但不在远端列表的自定义模型也渲染出来,保证可取消选中
-    for (const model of selectedModels) {
-      if (seen.has(model)) continue;
-      seen.add(model);
-      merged.push(model);
-    }
-
-    return merged;
-  })();
+  const displayedModels = useMemo(() => {
+    const seen = new Set<string>();
+    const merged: string[] = [];
+
+    for (const model of availableModels) {
+      if (seen.has(model)) continue;
+      seen.add(model);
+      merged.push(model);
+    }
+
+    // 关键:把已选中但不在远端列表的自定义模型也渲染出来,保证可取消选中
+    for (const model of selectedModels) {
+      if (seen.has(model)) continue;
+      seen.add(model);
+      merged.push(model);
+    }
+
+    return merged;
+  }, [availableModels, selectedModels]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 61 - 79, Replace the IIFE that computes displayedModels with a
useMemo hook so the Set construction and array traversals run only when
availableModels or selectedModels change; specifically, move the logic inside a
useMemo(() => { ... }, [availableModels, selectedModels]) and return the merged
array, keeping the same de-duplication and adding selectedModels not present in
availableModels so existing behavior of displayedModels, availableModels, and
selectedModels is preserved.

166-189: 建议将 SourceIndicator 提取到组件外部。

目前 SourceIndicatorModelMultiSelect 函数体内定义,每次父组件渲染都会产生新的函数引用。React 按引用比较组件类型,因此每次渲染都会 unmount 旧的 SourceIndicator 并 mount 新的,造成不必要的 DOM 重建,并可能导致 Tooltip 动画抖动。

建议将其提取为独立的具名组件(通过 props 传入 loadingmodelSource),或改写为返回 JSX 的普通辅助函数(通过变量而非组件标签调用)。

♻️ 建议改写为普通辅助函数(最小改动)
-  // 数据来源指示器
-  const SourceIndicator = () => {
-    if (loading) return null;
-
-    const isUpstream = modelSource === "upstream";
-    const Icon = isUpstream ? Cloud : Database;
-    const label = isUpstream ? t("sourceUpstream") : t("sourceFallback");
-    const description = isUpstream ? t("sourceUpstreamDesc") : t("sourceFallbackDesc");
-
-    return (
-      <TooltipProvider>
-        ...
-      </TooltipProvider>
-    );
-  };

在组件外部定义(接收显式 props):

+interface SourceIndicatorProps {
+  loading: boolean;
+  modelSource: ModelSource;
+  sourceUpstream: string;
+  sourceFallback: string;
+  sourceUpstreamDesc: string;
+  sourceFallbackDesc: string;
+}
+
+function SourceIndicator({
+  loading,
+  modelSource,
+  sourceUpstream,
+  sourceFallback,
+  sourceUpstreamDesc,
+  sourceFallbackDesc,
+}: SourceIndicatorProps) {
+  if (loading) return null;
+  const isUpstream = modelSource === "upstream";
+  const Icon = isUpstream ? Cloud : Database;
+  const label = isUpstream ? sourceUpstream : sourceFallback;
+  const description = isUpstream ? sourceUpstreamDesc : sourceFallbackDesc;
+  return (
+    <TooltipProvider>
+      ...
+    </TooltipProvider>
+  );
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx
around lines 166 - 189, The SourceIndicator function is defined inside
ModelMultiSelect causing remounts; extract it to a top-level named component or
convert it to a pure helper function that takes explicit props (loading,
modelSource, t) and returns JSX; reference the existing symbols
(SourceIndicator, ModelMultiSelect, loading, modelSource, t, Cloud, Database,
TooltipProvider/Tooltip/TooltipTrigger/TooltipContent) and update usages in
ModelMultiSelect to call the helper (e.g. SourceIndicator({loading, modelSource,
t}) or render <SourceIndicator loading={loading} modelSource={modelSource} t={t}
/>) so the component/function lives outside the parent render to avoid new refs
on each render.
src/components/form/client-restrictions-editor.tsx (1)

90-94: 建议对 suggestions 使用 useMemo 避免每次渲染重新构建

getPresetLabel 由父组件(routing-section.tsx 第 280-282 行)以内联箭头函数形式传入,每次父组件渲染都会产生新的函数引用,导致 suggestions 数组在每次渲染时都被重新映射。虽然数组仅有 4 个元素,但包裹 useMemo 是更好的实践。

♻️ 建议改为 useMemo
+import { useMemo } from "react";

 export function ClientRestrictionsEditor({ ..., getPresetLabel, ... }) {
-  const suggestions: TagInputSuggestion[] = CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => ({
-    value: option.value,
-    label: getPresetLabel(option.value),
-    keywords: [...option.aliases],
-  }));
+  const suggestions: TagInputSuggestion[] = useMemo(
+    () =>
+      CLIENT_RESTRICTION_PRESET_OPTIONS.map((option) => ({
+        value: option.value,
+        label: getPresetLabel(option.value),
+        keywords: [...option.aliases],
+      })),
+    [getPresetLabel],
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/form/client-restrictions-editor.tsx` around lines 90 - 94, The
suggestions array is rebuilt on every render because getPresetLabel is passed as
an inline function from the parent, so wrap the suggestions computation in
React.useMemo to memoize the mapped array; locate the const suggestions in
client-restrictions-editor.tsx and change it to compute
CLIENT_RESTRICTION_PRESET_OPTIONS.map(...) inside useMemo with getPresetLabel
and CLIENT_RESTRICTION_PRESET_OPTIONS as dependencies to avoid unnecessary
remapping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`[locale]/settings/providers/_components/model-multi-select.tsx:
- Around line 61-79: Replace the IIFE that computes displayedModels with a
useMemo hook so the Set construction and array traversals run only when
availableModels or selectedModels change; specifically, move the logic inside a
useMemo(() => { ... }, [availableModels, selectedModels]) and return the merged
array, keeping the same de-duplication and adding selectedModels not present in
availableModels so existing behavior of displayedModels, availableModels, and
selectedModels is preserved.
- Around line 166-189: The SourceIndicator function is defined inside
ModelMultiSelect causing remounts; extract it to a top-level named component or
convert it to a pure helper function that takes explicit props (loading,
modelSource, t) and returns JSX; reference the existing symbols
(SourceIndicator, ModelMultiSelect, loading, modelSource, t, Cloud, Database,
TooltipProvider/Tooltip/TooltipTrigger/TooltipContent) and update usages in
ModelMultiSelect to call the helper (e.g. SourceIndicator({loading, modelSource,
t}) or render <SourceIndicator loading={loading} modelSource={modelSource} t={t}
/>) so the component/function lives outside the parent render to avoid new refs
on each render.

In `@src/components/form/client-restrictions-editor.tsx`:
- Around line 90-94: The suggestions array is rebuilt on every render because
getPresetLabel is passed as an inline function from the parent, so wrap the
suggestions computation in React.useMemo to memoize the mapped array; locate the
const suggestions in client-restrictions-editor.tsx and change it to compute
CLIENT_RESTRICTION_PRESET_OPTIONS.map(...) inside useMemo with getPresetLabel
and CLIENT_RESTRICTION_PRESET_OPTIONS as dependencies to avoid unnecessary
remapping.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between a98ec3a and 5a1290d.

📒 Files selected for processing (3)
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
  • src/app/[locale]/settings/providers/_components/model-multi-select.tsx
  • src/components/form/client-restrictions-editor.tsx

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

82 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

maxTagLength={64}
maxTags={50}
disabled={disabled}
validateTag={() => true}
Copy link

Choose a reason for hiding this comment

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

validateTag={() => true} makes onInvalidTag partially dead code

Setting validateTag={() => true} bypasses the DEFAULT_TAG_PATTERN check in TagInput (see tag-input.tsx:228), so the invalid_format branch in the onInvalidTag handler passed from routing-section.tsx:288 will never fire. The base validation for empty, duplicate, too_long, and max_tags still works because it goes through validateBaseTag first, but format validation is silently skipped.

If the intent is to allow any format (since client restriction values can be arbitrary User-Agent substrings), this is acceptable — but consider removing invalid_format from the onInvalidTag handler in routing-section.tsx to avoid confusion, or add a comment here explaining why format validation is intentionally skipped.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/form/client-restrictions-editor.tsx
Line: 53

Comment:
**`validateTag={() => true}` makes `onInvalidTag` partially dead code**

Setting `validateTag={() => true}` bypasses the `DEFAULT_TAG_PATTERN` check in `TagInput` (see `tag-input.tsx:228`), so the `invalid_format` branch in the `onInvalidTag` handler passed from `routing-section.tsx:288` will never fire. The base validation for `empty`, `duplicate`, `too_long`, and `max_tags` still works because it goes through `validateBaseTag` first, but format validation is silently skipped.

If the intent is to allow any format (since client restriction values can be arbitrary User-Agent substrings), this is acceptable — but consider removing `invalid_format` from the `onInvalidTag` handler in `routing-section.tsx` to avoid confusion, or add a comment here explaining why format validation is intentionally skipped.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 56 to +59
targetIds: [] as number[],
};

beforeAll(async () => {
Copy link

Choose a reason for hiding this comment

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

beforeAll at module scope, outside run() block

Same pattern as api-complete.test.ts (which was already flagged in a prior thread): the beforeAll hook here runs at file scope, not inside the run(...) describe block. When ADMIN_KEY is absent, run is describe.skip, but beforeAll still executes (though it early-returns). Also in users-keys-complete.test.ts:189-192.

For consistency and proper scoping, both beforeAll and afterAll should be moved inside the run(...) block:

Suggested change
targetIds: [] as number[],
};
beforeAll(async () => {
run("通知设置 - Webhook 目标与绑定(E2E)", () => {
beforeAll(async () => {
if (!ADMIN_KEY) return;
authToken = await loginAndGetAuthToken(API_BASE_URL, ADMIN_KEY);
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/notification-settings.test.ts
Line: 56-59

Comment:
**`beforeAll` at module scope, outside `run()` block**

Same pattern as `api-complete.test.ts` (which was already flagged in a prior thread): the `beforeAll` hook here runs at file scope, not inside the `run(...)` describe block. When `ADMIN_KEY` is absent, `run` is `describe.skip`, but `beforeAll` still executes (though it early-returns). Also in `users-keys-complete.test.ts:189-192`.

For consistency and proper scoping, both `beforeAll` and `afterAll` should be moved inside the `run(...)` block:

```suggestion
run("通知设置 - Webhook 目标与绑定(E2E)", () => {
  beforeAll(async () => {
    if (!ADMIN_KEY) return;
    authToken = await loginAndGetAuthToken(API_BASE_URL, ADMIN_KEY);
  });
```

How can I resolve this? If you propose a fix, please make it concise.

@tesgth032 tesgth032 changed the title 修复服务商表单小屏/移动端自适应 [未完成] 修复服务商表单小屏/移动端自适应 Feb 23, 2026
@tesgth032
Copy link
Contributor Author

已按 CodeRabbit 的 nitpick 做了小幅性能优化(不改行为):

  • ModelMultiSelect:displayedModels 改为 useMemo;SourceIndicator 提到组件外(避免每次 render 新建)
  • ClientRestrictionsEditor:suggestions 用 useMemo;RoutingSection 传入的 getPresetLabel 用 useCallback 稳定引用

本地已跑:bun run typecheck / bun run test / bun run build 通过。

@tesgth032
Copy link
Contributor Author

补充修复本地 standalone 启动时静态资源 404 的问题:

  • 之前用 node .next/standalone/server.js 启动时,/_next/static/* 404 导致 CSS/JS 不加载(页面无样式)。根因是 Next standalone 需要 .next/staticpublicserver.js 同级存在。
  • 现在在 scripts/copy-version-to-standalone.cjs 中,build 后会自动把 .next/staticpublic 复制到 .next/standalone/,确保本地 standalone 直接可跑。

本机已验证:/_next/static/chunks/*.css 返回 200,页面样式恢复。

@tesgth032 tesgth032 changed the title 修复服务商表单小屏/移动端自适应 fix(ui): 服务商表单小屏体验与客户端限制配置 [未完成] Feb 23, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

87 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

maxTagLength={64}
maxTags={50}
disabled={disabled}
validateTag={() => true}
Copy link

Choose a reason for hiding this comment

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

validateTag={() => true} bypasses format validation - the invalid_format case in routing-section.tsx:288 onInvalidTag handler will never fire

if arbitrary UA strings are intentional, this is fine, but consider removing the dead invalid_format branch from the handler or adding a comment explaining why format validation is disabled

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/form/client-restrictions-editor.tsx
Line: 54

Comment:
`validateTag={() => true}` bypasses format validation - the `invalid_format` case in `routing-section.tsx:288` onInvalidTag handler will never fire

if arbitrary UA strings are intentional, this is fine, but consider removing the dead `invalid_format` branch from the handler or adding a comment explaining why format validation is disabled

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +77 to +78
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);
Copy link

Choose a reason for hiding this comment

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

hasAnyClientRestrictions computed on every render but only used during mount - use function initializer to avoid unnecessary computation:

Suggested change
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] =
useState(hasAnyClientRestrictions);
const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() =>
allowedClients.length > 0 || blockedClients.length > 0
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Line: 77-78

Comment:
`hasAnyClientRestrictions` computed on every render but only used during mount - use function initializer to avoid unnecessary computation:

```suggestion
  const [clientRestrictionsEnabled, setClientRestrictionsEnabled] = useState(() =>
    allowedClients.length > 0 || blockedClients.length > 0
  );
```

How can I resolve this? If you propose a fix, please make it concise.

animate={{
width: `${((TAB_CONFIG.findIndex((t) => t.id === activeTab) + 1) / TAB_CONFIG.length) * 100}%`,
}}
initial={{ width: stepProgressWidth }}
Copy link

Choose a reason for hiding this comment

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

progress bar animation disabled - both initial and animate set to same value stepProgressWidth, causing no visual transition when tabs change

remove initial prop to animate from current position:

Suggested change
initial={{ width: stepProgressWidth }}
animate={{ width: stepProgressWidth }}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
Line: 277

Comment:
progress bar animation disabled - both `initial` and `animate` set to same value `stepProgressWidth`, causing no visual transition when tabs change

remove `initial` prop to animate from current position:

```suggestion
            animate={{ width: stepProgressWidth }}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +59 to +66
});
} catch (error) {
lastError = error;
if (attempt >= maxAttempts) {
break;
}

await sleep(Math.min(1000, 100 * 2 ** (attempt - 1)));
Copy link

Choose a reason for hiding this comment

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

catch block retries on ALL fetch errors (network failures, DNS issues, timeouts) - masks legitimate test setup problems and causes tests to hang ~6s instead of failing fast

only retry specific recoverable errors like ECONNRESET/ETIMEDOUT, or remove this catch since line 82-93 already handles HTTP 503 retries

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/e2e/_helpers/auth.ts
Line: 59-66

Comment:
catch block retries on ALL fetch errors (network failures, DNS issues, timeouts) - masks legitimate test setup problems and causes tests to hang ~6s instead of failing fast

only retry specific recoverable errors like `ECONNRESET`/`ETIMEDOUT`, or remove this catch since line 82-93 already handles HTTP 503 retries

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

88 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 24, 2026

Additional Comments (1)

src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Info icon positioned inside Select breaks interaction - clicking the icon area won't open the select dropdown because it's overlaid on top

the tooltip wraps the entire div containing the Select, so hovering anywhere (including the select trigger) will show the tooltip, which conflicts with the select's own UI

move the Info icon outside the Select and only wrap the icon with the tooltip trigger

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
Line: 550

Comment:
`Info` icon positioned inside `Select` breaks interaction - clicking the icon area won't open the select dropdown because it's overlaid on top

the tooltip wraps the entire `div` containing the `Select`, so hovering anywhere (including the select trigger) will show the tooltip, which conflicts with the select's own UI

move the `Info` icon outside the `Select` and only wrap the icon with the tooltip trigger

How can I resolve this? If you propose a fix, please make it concise.

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

Labels

area:provider area:UI bug Something isn't working size/S Small PR (< 200 lines)

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant