Skip to content

fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误#435

Merged
claude-code-best merged 1 commit intoclaude-code-best:mainfrom
bonerush:fix/conditional-hooks-ctrlo-error
May 8, 2026
Merged

fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误#435
claude-code-best merged 1 commit intoclaude-code-best:mainfrom
bonerush:fix/conditional-hooks-ctrlo-error

Conversation

@bonerush
Copy link
Copy Markdown
Contributor

@bonerush bonerush commented May 8, 2026

修复在 dev 模式下按下 Ctrl+O 切换 transcript 视图时 React 抛出
"Rendered fewer hooks than expected" 崩溃的问题。

Fixes #434

根因分析

项目中有大量 hook(useState / useMemo / useRef / useSyncExternalStore 等) 被包裹在 feature() 三元表达式中条件调用,例如:

const value = feature('X') ? useHook() : defaultValue;

在 build 模式下 feature() 是编译时常量,死代码消除会移除未使用的分支,
hooks 数量在编译后是确定的。但在 dev 模式下(scripts/dev.ts 注入
--feature 启用全部 31 个 feature),feature() 是运行时调用,
但始终返回 true,因此所有 hooks 都会被调用,原本不会出问题。

真正的触发器是 REPL.tsx 第 5381 行的提前返回:

if (screen === 'transcript') { return transcriptReturn; }

当用户按下 Ctrl+O 进入 transcript 模式时,该提前返回之后的所有 hooks
(如 displayedAgentMessages 的 useMemo)都不会被调用,导致 React 在 下一次渲染时检测到 hooks 数量与上次不一致而崩溃。

此外,其他文件中也存在相同的条件式 hook 模式——虽然 dev 模式下
feature() 返回 true,所以这些路径实际上不会被触发,但它们是
潜在的隐患:若将来有人通过环境变量关闭某个 feature,
同样的崩溃会立即出现。

修复策略

采用统一模式:始终无条件调用 hook,将 feature() gate 应用到值上

// Before (unsafe — hook count varies by feature flag)
const value = feature('X') ? useHook() : defaultValue;

// After (safe — hook always called, gate on the value)
const rawValue = useHook();
const value = feature('X') ? rawValue : defaultValue;

修改清单

核心修复(REPL.tsx)

  • displayedAgentMessages useMemo 及依赖变量(viewedTask / viewedTeammateTask / viewedAgentTask / usesSyncMessages / rawAgentMessages / displayedMessages)从 transcript 提前返回 之后移至之前,确保两模式下 hooks 调用顺序一致
  • 修复 disableMessageActions / useAssistantHistory / voiceIntegration 的条件式 hook 调用

条件式 hook 修复(11 个文件)

  • src/hooks/useGlobalKeybindings.tsx — isBriefOnly / toggleBrief keybinding 改为 isActive 门控
  • src/hooks/useReplBridge.tsx — 5 个 BRIDGE_MODE 选值改为无条件调用
  • src/hooks/useVoiceIntegration.tsx — 4 个 VOICE_MODE 选值修复
  • src/components/PromptInput/Notifications.tsx — 4 个 feature 选值修复
  • src/components/PromptInput/PromptInput.tsx — briefOwnsGap / companionSpeaking 修复
  • src/components/PromptInput/PromptInputFooterLeftSide.tsx — 4 个 VOICE_MODE 选值修复
  • src/components/PromptInput/PromptInputQueuedCommands.tsx — isBriefOnly
  • src/components/Spinner.tsx — briefEnvEnabled 修复
  • src/components/TextInput.tsx — voiceState / audioLevels / animationFrame 修复
  • src/components/messages/AttachmentMessage.tsx — isDemoEnv 修复
  • src/components/messages/UserPromptMessage.tsx — isBriefOnly / viewingAgentTaskId / briefEnvEnabled 修复
  • src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx — isBriefOnly 修复

其他修复

  • src/components/FeedbackSurvey/useFrustrationDetection.ts — 将 3 个 提前返回合并为 shouldSkip 变量,handleTranscriptSelect 提前 return
  • src/hooks/useIssueFlagBanner.ts — useRef 移到 USER_TYPE 检查之前
  • src/hooks/useUpdateNotification.ts — useState 改为 useRef, 避免版本号变化触发不必要重渲染

构建/开发配置

  • build.ts — 添加 sourcemap: 'linked'
  • scripts/dev.ts — NODE_ENV 从 'production' 改为 'development'

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by CodeRabbit

  • Chores

    • Enhanced build tooling with source map support for improved debugging capability.
    • Improved development environment configuration.
  • Refactor

    • Restructured internal state management patterns to improve reliability and consistency.
    • Optimized feature flag handling across the application for more predictable behavior.

修复在 dev 模式下按下 Ctrl+O 切换 transcript 视图时 React 抛出
"Rendered fewer hooks than expected" 崩溃的问题。

## 根因分析

项目中有大量 hook(useState / useMemo / useRef / useSyncExternalStore 等)
被包裹在 `feature()` 三元表达式中条件调用,例如:

    const value = feature('X') ? useHook() : defaultValue;

在 build 模式下 `feature()` 是编译时常量,死代码消除会移除未使用的分支,
hooks 数量在编译后是确定的。但在 dev 模式下(scripts/dev.ts 注入
--feature 启用全部 31 个 feature),`feature()` 是运行时调用,
但始终返回 true,因此所有 hooks 都会被调用,原本不会出问题。

真正的触发器是 REPL.tsx 第 5381 行的提前返回:

    if (screen === 'transcript') { return transcriptReturn; }

当用户按下 Ctrl+O 进入 transcript 模式时,该提前返回之后的所有 hooks
(如 displayedAgentMessages 的 useMemo)都不会被调用,导致 React 在
下一次渲染时检测到 hooks 数量与上次不一致而崩溃。

此外,其他文件中也存在相同的条件式 hook 模式——虽然 dev 模式下
feature() 返回 true,所以这些路径实际上不会被触发,但它们是
潜在的隐患:若将来有人通过环境变量关闭某个 feature,
同样的崩溃会立即出现。

## 修复策略

采用统一模式:**始终无条件调用 hook,将 feature() gate 应用到值上**。

    // Before (unsafe — hook count varies by feature flag)
    const value = feature('X') ? useHook() : defaultValue;

    // After (safe — hook always called, gate on the value)
    const rawValue = useHook();
    const value = feature('X') ? rawValue : defaultValue;

## 修改清单

### 核心修复(REPL.tsx)
- 将 `displayedAgentMessages` useMemo 及依赖变量(viewedTask /
  viewedTeammateTask / viewedAgentTask / usesSyncMessages /
  rawAgentMessages / displayedMessages)从 transcript 提前返回
  之后移至之前,确保两模式下 hooks 调用顺序一致
- 修复 `disableMessageActions` / `useAssistantHistory` /
  `voiceIntegration` 的条件式 hook 调用

### 条件式 hook 修复(11 个文件)
- src/hooks/useGlobalKeybindings.tsx — isBriefOnly / toggleBrief
  keybinding 改为 isActive 门控
- src/hooks/useReplBridge.tsx — 5 个 BRIDGE_MODE 选值改为无条件调用
- src/hooks/useVoiceIntegration.tsx — 4 个 VOICE_MODE 选值修复
- src/components/PromptInput/Notifications.tsx — 4 个 feature 选值修复
- src/components/PromptInput/PromptInput.tsx — briefOwnsGap /
  companionSpeaking 修复
- src/components/PromptInput/PromptInputFooterLeftSide.tsx — 4 个
  VOICE_MODE 选值修复
- src/components/PromptInput/PromptInputQueuedCommands.tsx — isBriefOnly
- src/components/Spinner.tsx — briefEnvEnabled 修复
- src/components/TextInput.tsx — voiceState / audioLevels /
  animationFrame 修复
- src/components/messages/AttachmentMessage.tsx — isDemoEnv 修复
- src/components/messages/UserPromptMessage.tsx — isBriefOnly /
  viewingAgentTaskId / briefEnvEnabled 修复
- src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx
  — isBriefOnly 修复

### 其他修复
- src/components/FeedbackSurvey/useFrustrationDetection.ts — 将 3 个
  提前返回合并为 shouldSkip 变量,handleTranscriptSelect 提前 return
- src/hooks/useIssueFlagBanner.ts — useRef 移到 USER_TYPE 检查之前
- src/hooks/useUpdateNotification.ts — useState 改为 useRef,
  避免版本号变化触发不必要重渲染

### 构建/开发配置
- build.ts — 添加 `sourcemap: 'linked'`
- scripts/dev.ts — NODE_ENV 从 'production' 改为 'development'

Closes claude-code-best#434
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR systematically refactors React hook invocation patterns across the codebase to avoid conditional hook calls. Instead of calling hooks only when feature flags are enabled, hooks are now invoked unconditionally and feature gates are applied to their return values. The changes span build configuration, voice mode, brief mode, and state management while preserving all existing runtime behavior.

Changes

Hook Pattern Refactoring

Layer / File(s) Summary
Build & Dev Configuration
build.ts, scripts/dev.ts
Build adds sourcemap: 'linked' option; dev script sets NODE_ENV to "development" instead of "production".
Hook State Refactoring Framework
src/hooks/useUpdateNotification.ts
Replaces useState with useRef for tracking last-notified semver, avoiding re-renders when version is already known.
Brief Mode Hook Consolidation
src/components/PromptInput/PromptInput.tsx, src/components/PromptInput/PromptInputQueuedCommands.tsx, src/components/Spinner.tsx, src/components/messages/UserPromptMessage.tsx, src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx, src/hooks/useGlobalKeybindings.tsx
isBriefOnly and briefEnvEnabled are read via unconditional useAppState/useMemo calls; KAIROS/KAIROS_BRIEF feature gates are applied to derived boolean values instead of controlling hook invocation. Buddy reaction state is refactored to store companionReaction in a local selector and derive companionSpeaking from it.
Voice Mode Hook Consolidation
src/components/PromptInput/Notifications.tsx, src/components/PromptInput/PromptInputFooterLeftSide.tsx, src/components/TextInput.tsx, src/hooks/useVoiceIntegration.tsx
useVoiceState and useVoiceEnabled are called unconditionally; VOICE_MODE feature gate is applied to resulting values with fallbacks ('idle', false, []) when disabled.
Bridge Mode & Environment Feature Gating
src/hooks/useReplBridge.tsx, src/hooks/useIssueFlagBanner.ts, src/components/messages/AttachmentMessage.tsx
Bridge flags, user-type checks, and demo-environment flag are derived from unconditional AppState reads and environment variables, with feature gates applied to computed values.
Screen Integration & Rendering
src/screens/REPL.tsx
Refactors how disableMessageActions, useVoiceIntegration, and useAssistantHistory are invoked; updates viewed-task variable derivation for transcript-mode rendering.
Core Logic Refinement
src/components/FeedbackSurvey/useFrustrationDetection.ts
Consolidates skip conditions (policyAllowed, dismissed config, loading, active prompt, other survey open) into a single shouldSkip boolean; updates effectiveState and handleTranscriptSelect to respect the skip gate.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hooks unconditional, logic flows so clear,
No feature gates wrapping the calls we hear,
Fallbacks aplenty when flags say "no way,"
React rules happy, hooray, hooray!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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 Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately describes the main objective: fixing a React error caused by conditional hook calls. It directly relates to the primary change across all modified files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
build.ts (1)

65-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset regex state before each .test() call inside the file loop.

The global regex /g with .test() maintains state via lastIndex. The current code resets it after the loop completes, but this happens too late—files processed later in the loop may start searching from an incorrect position, causing matches to be skipped. Move the BUN_DESTRUCTURE.lastIndex = 0 reset inside the loop before the .test() call to ensure each file is checked from the beginning.

Proposed fix
 for (const file of files) {
   if (!file.endsWith('.js')) continue
   const filePath = join(outdir, file)
   const content = await readFile(filePath, 'utf-8')
+  BUN_DESTRUCTURE.lastIndex = 0
   if (BUN_DESTRUCTURE.test(content)) {
     await writeFile(
       filePath,
       content.replace(BUN_DESTRUCTURE, BUN_DESTRUCTURE_SAFE),
     )
     bunPatched++
   }
 }
-BUN_DESTRUCTURE.lastIndex = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.ts` around lines 65 - 73, The BUN_DESTRUCTURE regex is global and its
.test() call inside the for loop can skip matches due to lastIndex state; inside
the loop (before calling BUN_DESTRUCTURE.test(content)) reset
BUN_DESTRUCTURE.lastIndex = 0 so each file is tested from the start, then
continue using BUN_DESTRUCTURE_SAFE and writeFile as before (affects the code
around the for (const file of files) loop, the
BUN_DESTRUCTURE/BUN_DESTRUCTURE_SAFE variables, and the readFile/writeFile
logic).
🧹 Nitpick comments (3)
src/components/FeedbackSurvey/useFrustrationDetection.ts (3)

15-15: ⚡ Quick win

as any cast violates the no-any guideline — use a type guard instead.

(m as any).isApiErrorMessage bypasses type safety. The proper fix is a type guard or an interface extension.

♻️ Proposed fix
+function isApiError(m: Message): m is Message & { isApiErrorMessage: true } {
+  return (m as unknown as { isApiErrorMessage?: boolean }).isApiErrorMessage === true
+}
+
 function detectFrustration(messages: Message[]): boolean {
-  const apiErrors = messages.filter(m => (m as any).isApiErrorMessage)
+  const apiErrors = messages.filter(isApiError)
   return apiErrors.length >= 2
 }

As per coding guidelines: "Prohibit as any type assertions in production code; use as unknown as SpecificType double assertion or interface supplementation when type mismatches occur."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FeedbackSurvey/useFrustrationDetection.ts` at line 15, Replace
the unsafe cast in useFrustrationDetection.ts: instead of (m as
any).isApiErrorMessage, implement a proper type guard function (e.g., function
isApiErrorMessage(msg: unknown): msg is ApiErrorMessage) and use it in the
filter that produces apiErrors (const apiErrors =
messages.filter(isApiErrorMessage)). Define or extend the ApiErrorMessage
interface to include the isApiErrorMessage property so the type guard can narrow
types safely and remove the as any assertion.

44-56: ⚡ Quick win

handleTranscriptSelect should be wrapped in useCallback to stabilise its reference.

The function is recreated on every render. Any consumer that passes it as a prop or dependency will re-render unnecessarily. The dependencies are shouldSkip, messages, and setState.

♻️ Proposed fix
-  const handleTranscriptSelect = (choice: string) => {
+  const handleTranscriptSelect = useCallback((choice: string) => {
     if (shouldSkip) return
     if (choice === 'yes') {
       void submitTranscriptShare(messages, 'frustration', crypto.randomUUID())
       setState('submitted')
     } else {
-      saveGlobalConfig((current: any) => ({
+      saveGlobalConfig((current: Record<string, unknown>) => ({
         ...current,
         transcriptShareDismissed: true,
       }))
       setState('closed')
     }
-  }
+  }, [shouldSkip, messages])

Also add useCallback to the import at line 1:

-import { useState } from 'react'
+import { useState, useCallback } from 'react'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FeedbackSurvey/useFrustrationDetection.ts` around lines 44 -
56, handleTranscriptSelect is recreated on every render; wrap it with React's
useCallback and add useCallback to the component imports so its reference is
stable. Change the declaration of handleTranscriptSelect to useCallback(() => {
... }, [shouldSkip, messages, setState]) and keep the internal logic (calls to
submitTranscriptShare, saveGlobalConfig and crypto.randomUUID()) unchanged;
ensure the dependency array includes shouldSkip, messages and setState so the
callback updates correctly.

50-50: ⚡ Quick win

current: any in the saveGlobalConfig callback violates the no-any guideline.

♻️ Proposed fix
-      saveGlobalConfig((current: any) => ({
+      saveGlobalConfig((current: Record<string, unknown>) => ({
         ...current,
         transcriptShareDismissed: true,
       }))

As per coding guidelines: "Use Record<string, unknown> instead of any for objects with unknown structure."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FeedbackSurvey/useFrustrationDetection.ts` at line 50, The
callback passed to saveGlobalConfig uses the parameter typed as any (current:
any); replace that with a safer type per guidelines—use current: Record<string,
unknown> (or a more specific interface if available) in the saveGlobalConfig
callback inside useFrustrationDetection.ts so the callback signature avoids any
while preserving the existing logic and return shape for saveGlobalConfig.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/dev.ts`:
- Around line 19-21: Update the stale comment above the process.env.NODE_ENV
definition in scripts/dev.ts to reflect that NODE_ENV is set to "development"
(not production); locate the comment immediately preceding the
'process.env.NODE_ENV': JSON.stringify('development') entry and change the text
so it accurately describes the development-mode behavior (e.g., mention React
development mode and that this prevents accumulation of _debugStack Error
objects during long-running sessions).

In `@src/components/FeedbackSurvey/useFrustrationDetection.ts`:
- Line 28: The code uses a banned "as any" cast when calling isPolicyAllowed;
replace it by either adding 'product_feedback' to the PolicyKey union type if
it's a valid policy key, or perform a safe double-cast like `'product_feedback'
as unknown as PolicyKey` when calling isPolicyAllowed (update the call site
where policyAllowed is assigned and ensure isPolicyAllowed's parameter type is
PolicyKey); remove the "as any" cast and run type checks to confirm no remaining
mismatches.

In `@src/hooks/useUpdateNotification.ts`:
- Around line 22-23: Validate updatedVersion with semver.valid(...) using the
same loose option before calling getSemverPart to avoid exceptions; in the
useUpdateNotification flow replace the direct call const updatedSemver =
updatedVersion ? getSemverPart(updatedVersion) : null with a validation step
(e.g., const isValid = semver.valid(updatedVersion, { loose: true })) and only
call getSemverPart(updatedVersion) when isValid is true, otherwise set
updatedSemver to null so the subsequent if (!updatedSemver) branch is safe.

---

Outside diff comments:
In `@build.ts`:
- Around line 65-73: The BUN_DESTRUCTURE regex is global and its .test() call
inside the for loop can skip matches due to lastIndex state; inside the loop
(before calling BUN_DESTRUCTURE.test(content)) reset BUN_DESTRUCTURE.lastIndex =
0 so each file is tested from the start, then continue using
BUN_DESTRUCTURE_SAFE and writeFile as before (affects the code around the for
(const file of files) loop, the BUN_DESTRUCTURE/BUN_DESTRUCTURE_SAFE variables,
and the readFile/writeFile logic).

---

Nitpick comments:
In `@src/components/FeedbackSurvey/useFrustrationDetection.ts`:
- Line 15: Replace the unsafe cast in useFrustrationDetection.ts: instead of (m
as any).isApiErrorMessage, implement a proper type guard function (e.g.,
function isApiErrorMessage(msg: unknown): msg is ApiErrorMessage) and use it in
the filter that produces apiErrors (const apiErrors =
messages.filter(isApiErrorMessage)). Define or extend the ApiErrorMessage
interface to include the isApiErrorMessage property so the type guard can narrow
types safely and remove the as any assertion.
- Around line 44-56: handleTranscriptSelect is recreated on every render; wrap
it with React's useCallback and add useCallback to the component imports so its
reference is stable. Change the declaration of handleTranscriptSelect to
useCallback(() => { ... }, [shouldSkip, messages, setState]) and keep the
internal logic (calls to submitTranscriptShare, saveGlobalConfig and
crypto.randomUUID()) unchanged; ensure the dependency array includes shouldSkip,
messages and setState so the callback updates correctly.
- Line 50: The callback passed to saveGlobalConfig uses the parameter typed as
any (current: any); replace that with a safer type per guidelines—use current:
Record<string, unknown> (or a more specific interface if available) in the
saveGlobalConfig callback inside useFrustrationDetection.ts so the callback
signature avoids any while preserving the existing logic and return shape for
saveGlobalConfig.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51e4a7c7-13ff-4bf5-a8bd-3a4384ef5147

📥 Commits

Reviewing files that changed from the base of the PR and between 73e54d4 and 8ba51ed.

📒 Files selected for processing (18)
  • build.ts
  • scripts/dev.ts
  • src/components/FeedbackSurvey/useFrustrationDetection.ts
  • src/components/PromptInput/Notifications.tsx
  • src/components/PromptInput/PromptInput.tsx
  • src/components/PromptInput/PromptInputFooterLeftSide.tsx
  • src/components/PromptInput/PromptInputQueuedCommands.tsx
  • src/components/Spinner.tsx
  • src/components/TextInput.tsx
  • src/components/messages/AttachmentMessage.tsx
  • src/components/messages/UserPromptMessage.tsx
  • src/components/messages/UserToolResultMessage/UserToolSuccessMessage.tsx
  • src/hooks/useGlobalKeybindings.tsx
  • src/hooks/useIssueFlagBanner.ts
  • src/hooks/useReplBridge.tsx
  • src/hooks/useUpdateNotification.ts
  • src/hooks/useVoiceIntegration.tsx
  • src/screens/REPL.tsx

Comment thread scripts/dev.ts
Comment on lines 19 to +21
// React production mode — prevents 6,889+ _debugStack Error objects
// (12MB) from accumulating during long-running sessions.
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.NODE_ENV': JSON.stringify('development'),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale comment to match NODE_ENV="development" behavior.

Line 19 still says “React production mode”, but Line 21 now sets development. Please align the comment to avoid misleading future changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/dev.ts` around lines 19 - 21, Update the stale comment above the
process.env.NODE_ENV definition in scripts/dev.ts to reflect that NODE_ENV is
set to "development" (not production); locate the comment immediately preceding
the 'process.env.NODE_ENV': JSON.stringify('development') entry and change the
text so it accurately describes the development-mode behavior (e.g., mention
React development mode and that this prevents accumulation of _debugStack Error
objects during long-running sessions).

if (isLoading || hasActivePrompt || otherSurveyOpen) {
return { state: 'closed', handleTranscriptSelect: () => {} }
}
const policyAllowed = isPolicyAllowed('product_feedback' as any)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

as any cast on isPolicyAllowed argument indicates a type mismatch that should be resolved properly.

'product_feedback' as any silences a compile error rather than fixing it. Either the accepted policy key union type should include 'product_feedback', or a as unknown as PolicyKey double-cast should be used per guidelines.

♻️ Proposed fix (if `PolicyKey` is the union type)
-  const policyAllowed = isPolicyAllowed('product_feedback' as any)
+  const policyAllowed = isPolicyAllowed('product_feedback' as unknown as PolicyKey)

Alternatively, add 'product_feedback' to the PolicyKey union type if it is a legitimate key.

As per coding guidelines: "Prohibit as any type assertions in production code; use as unknown as SpecificType double assertion."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const policyAllowed = isPolicyAllowed('product_feedback' as any)
const policyAllowed = isPolicyAllowed('product_feedback' as unknown as PolicyKey)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FeedbackSurvey/useFrustrationDetection.ts` at line 28, The
code uses a banned "as any" cast when calling isPolicyAllowed; replace it by
either adding 'product_feedback' to the PolicyKey union type if it's a valid
policy key, or perform a safe double-cast like `'product_feedback' as unknown as
PolicyKey` when calling isPolicyAllowed (update the call site where
policyAllowed is assigned and ensure isPolicyAllowed's parameter type is
PolicyKey); remove the "as any" cast and run type checks to confirm no remaining
mismatches.

Comment on lines +22 to +23
const updatedSemver = updatedVersion ? getSemverPart(updatedVersion) : null
if (!updatedSemver) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In semver v7, what do major()/minor()/patch() do when passed an invalid version string? Do they throw or return null?

💡 Result:

In semver v7 (npm/node-semver package version 7.x), the major, minor, and patch functions throw a TypeError with message "Invalid Version: {version}" when passed an invalid version string. They do not return null.[1][2][3]

Citations:


🏁 Script executed:

cat -n src/hooks/useUpdateNotification.ts

Repository: claude-code-best/claude-code

Length of output: 1270


🌐 Web query:

semver v7 loose option behavior major minor patch invalid version

💡 Result:

In npm's node-semver library version 7.x (latest 7.7.4 as of 2026), the 'loose' option makes parsing more forgiving for not-quite-valid semver strings, primarily by allowing prerelease tags without the required '-' separator (e.g., semver.valid('1.2.3beta', {loose: true}) returns '1.2.3-beta' [1][2][3]). Output is always strict-compliant semver. For major, minor, patch extraction: Functions like major(v), minor(v), patch(v) first parse/validate v using loose rules if specified, then extract the respective component from the parsed SemVer object [4][2]. If invalid even loosely, they return undefined/null. Invalid versions: semver.valid('a.b.c') returns null even with loose [4]. Loose does not accept non-numeric components in major/minor/patch or malformed structures like missing parts ('2.0' likely null in loose [5]). It may misparse extras like '1.2.34.5' as '1.2.3-4.5' by treating extra as prerelease [5]. No major changes to loose behavior in v7; recent fixes ensure CLI passes loose to valid [1][6]. Use coerce for aggressive non-semver to semver conversion (e.g., '42.6.7.9.3-alpha' -> '42.6.7'), independent of loose [4][7].

Citations:


🌐 Web query:

semver.major() with loose:true throws or returns null invalid version

💡 Result:

In the npm/node-semver library, semver.major(v) returns a major number (it’s not documented to return null for invalid versions), and loose:true is only relevant when parsing/validating options are actually used by the underlying logic. The public semver docs state that valid(v) returns null if it’s not valid, while major(v) “Return the major version number” (no null/loose behavior described for major specifically) [1]. So the behavior “semver.major with { loose:true } throws or returns null for an invalid version” is not a specific/known “bug” in the major API itself. What can happen is: 1) If the input string is not valid semver, the code path may throw a TypeError “Invalid Version: …” (common across semver operations when invalid inputs are passed to logic that expects a valid SemVer object) [2][3]. 2) If you use the CLI or other helper code that calls semver.valid(v, options), loose:true can change validation results (for example, the npm CLI had a loose-flag validation issue because it called semver.valid(v) without passing the loose option, and it was fixed to call semver.valid(v, options)) [4]. Practical takeaway: don’t rely on semver.major(v, { loose:true }) returning null for invalid input. If you need “null instead of throw,” call semver.valid(v, { loose: true }) first; docs explicitly say valid(v) “Return the parsed version, or null if it’s not valid” [1]. Minimal example pattern (node-semver): - if (!semver.valid(input, { loose: true })) return null; - else return semver.major(input); Citations: semver.valid returns parsed version or null [1]; semver.major is described as returning the major version number [1]; npm CLI loose validation bug/fix indicates passing loose into semver.valid is what changes loose behavior [4]; invalid version commonly throws TypeError in semver operations [2][3].

Citations:


Guard semver parsing before calling getSemverPart to avoid render-time crashes.

A truthy but invalid updatedVersion will throw via major()/minor()/patch() at line 22 during render, even with { loose: true }. The loose option only affects validation; it does not prevent exceptions when parsing malformed versions. Validate first using semver.valid() with the same loose option, then parse only if valid.

Proposed fix
-import { major, minor, patch } from 'semver'
+import { major, minor, patch, valid } from 'semver'
@@
-  const updatedSemver = updatedVersion ? getSemverPart(updatedVersion) : null
+  const normalizedUpdatedVersion =
+    updatedVersion && valid(updatedVersion, { loose: true })
+      ? updatedVersion
+      : null
+  const updatedSemver = normalizedUpdatedVersion ? getSemverPart(normalizedUpdatedVersion) : null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updatedSemver = updatedVersion ? getSemverPart(updatedVersion) : null
if (!updatedSemver) {
const normalizedUpdatedVersion =
updatedVersion && valid(updatedVersion, { loose: true })
? updatedVersion
: null
const updatedSemver = normalizedUpdatedVersion ? getSemverPart(normalizedUpdatedVersion) : null
if (!updatedSemver) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useUpdateNotification.ts` around lines 22 - 23, Validate
updatedVersion with semver.valid(...) using the same loose option before calling
getSemverPart to avoid exceptions; in the useUpdateNotification flow replace the
direct call const updatedSemver = updatedVersion ? getSemverPart(updatedVersion)
: null with a validation step (e.g., const isValid =
semver.valid(updatedVersion, { loose: true })) and only call
getSemverPart(updatedVersion) when isValid is true, otherwise set updatedSemver
to null so the subsequent if (!updatedSemver) branch is safe.

@bonerush bonerush changed the title fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误 fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误 #434 May 8, 2026
@bonerush bonerush changed the title fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误 #434 fix: 修复条件式 hook 调用导致的 "Rendered fewer hooks than expected" 错误 May 8, 2026
@bonerush bonerush closed this May 8, 2026
@bonerush bonerush reopened this May 8, 2026
@claude-code-best claude-code-best merged commit 02dd796 into claude-code-best:main May 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

当在对话界面摁下ctrl+o的时候会出现报错

2 participants