Repository navigation
feat(wework): persist scoped model selection - #1218
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough<review_stack_artifact> </review_stack_artifact> ✨ Finishing Touches🧪 Generate unit tests (beta)
|
d2349b4 to
1dde628
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
executor/agents/claude_code/standalone_chat_workspace.py (1)
48-52: ⚡ Quick winRedundant wrapper function.
slugify_responseis a simple pass-through toslugify_workspace_namewith no additional logic. Consider removing this wrapper and updating callers to useslugify_workspace_namedirectly, or add a docstring explaining why both functions are needed.♻️ Simplify by removing wrapper
Update line 70 to call
slugify_workspace_namedirectly:def unique_chat_workspace_path(root: Path, response_text: str) -> Path: """Return an unused dated workspace path for the response text.""" date_dir = root / datetime.now().strftime("%Y-%m-%d") - base_name = slugify_workspace_name(response_text) + base_name = slugify_workspace_name(response_text)Then remove the wrapper:
-def slugify_response(response_text: str) -> str: - """Build a filesystem-safe slug from response words.""" - - return slugify_workspace_name(response_text) -🤖 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 `@executor/agents/claude_code/standalone_chat_workspace.py` around lines 48 - 52, The wrapper function slugify_response is redundant because it simply forwards to slugify_workspace_name; remove slugify_response and update all callers to call slugify_workspace_name directly (replace any uses of slugify_response with slugify_workspace_name). Alternatively, if you prefer to keep the wrapper for API clarity, add a short docstring explaining the distinction and ensure tests/call sites still use slugify_response intentionally; otherwise delete slugify_response and run the test suite to confirm no references remain.wework/src/api/projects.ts (1)
13-18: ⚡ Quick winConsider sharing
WEWORK_CLIENT_ORIGINandwithClientOriginto avoid divergence.
WEWORK_CLIENT_ORIGINis duplicated intasks.ts(Line 11), which additionally re-implements the same query-appending logic by hand instead of reusing a helper. Extracting both into a shared module (e.g. a smallclientOrigin.ts) keeps the marker and the append logic consistent acrossprojects.tsandtasks.ts.🤖 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 `@wework/src/api/projects.ts` around lines 13 - 18, Extract the duplicated WEWORK_CLIENT_ORIGIN constant and withClientOrigin function into a small shared module (e.g., clientOrigin.ts) that exports named WEWORK_CLIENT_ORIGIN and withClientOrigin; then remove the duplicate declaration and inline logic from both projects.ts and tasks.ts and import { WEWORK_CLIENT_ORIGIN, withClientOrigin } from the new module, making sure the helper preserves the existing behavior of appending "client_origin=wework" with the correct '?'/'&' separator.wework/src/hooks/useEscapeKey.ts (1)
3-15: 💤 Low valueConsider using
useEffectEventto avoid re-subscribing the keydown listener whenonEscapechanges.Since
onEscapeis in the Effect dependency array, callers passing inline callbacks will trigger listener detach/attach every render. React 19.2 exportsuseEffectEventfrom'react'(import { useEffectEvent } from 'react'), so you can wraponEscapeand keep the Effect dependencies minimal (e.g., justenabled).🤖 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 `@wework/src/hooks/useEscapeKey.ts` around lines 3 - 15, The useEscapeKey hook currently re-subscribes the keydown listener whenever the onEscape callback changes; wrap onEscape with React's useEffectEvent (import { useEffectEvent } from 'react') inside useEscapeKey to produce a stable callback used by handleKeyDown, then make the useEffect dependency list only [enabled] so the listener isn't torn down/recreated on every onEscape identity change; keep handleKeyDown using the effect event-wrapped callback and retain the existing add/remove listener logic.wework/src/features/workbench/WorkbenchProvider.tsx (1)
536-581: 💤 Low valueUse
detailTaskconsistently.Line 539 creates
detailTaskbut line 557 repeats the cast withdetail as Task. For consistency and readability, use thedetailTaskvariable throughout.♻️ Suggested consistency fix
dispatch({ type: 'task_opened', - task: detail as Task, + task: detailTask, project,🤖 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 `@wework/src/features/workbench/WorkbenchProvider.tsx` around lines 536 - 581, openTask creates detailTask but the function still uses `detail as Task` and `detail.subtasks` in places — replace those with the already-casted `detailTask` to be consistent and readable: use `detailTask` for the `task` field in the `dispatch({ type: 'task_opened', task: ... })` call and for the subtasks passed to `sortSubtasksForDisplay(...).map(subtaskToMessage)` (i.e., replace `detail as Task` and `detail.subtasks` with `detailTask` and `detailTask.subtasks`), keeping the rest of the logic (standaloneDeviceId logic, resolvedProjectId handling, and `resolvedServices.chatStream.joinTask(taskId)`) unchanged.backend/app/models/task.py (1)
97-103: ⚡ Quick winRefine the
client_originindexing suggestion fortasks
tasks.client_originhas no column-levelindex=True, butbackend/alembic/versions/20260529_e6f7a8b9c012_add_client_origin.pyalready adds a composite indexix_tasks_user_origin_active_project (user_id, client_origin, is_active, project_id).- Some queries filter by
TaskResource.client_originwithproject_id/is_activebut omituser_id, so that composite index may not help; if this path is hot, consider an additional index that matches the actual predicates (e.g.,(client_origin, is_active, project_id)or(client_origin, is_active)) based on query/explain output.🤖 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 `@backend/app/models/task.py` around lines 97 - 103, The Task model's client_origin column lacks a standalone/indexed column and the existing migration only creates a composite index (ix_tasks_user_origin_active_project on user_id, client_origin, is_active, project_id), which won't support queries that filter by client_origin with project_id/is_active but omit user_id; add an index that matches common query predicates by either adding index=True to the client_origin Column in the Task model or (preferably) creating a dedicated Index (e.g., on (client_origin, is_active, project_id) or (client_origin, is_active)) in the model and adding a corresponding alembic migration (referencing the Task model's client_origin and the existing migration file 20260529_e6f7a8b9c012_add_client_origin.py) so the DB has an index that matches the actual query patterns observed.backend/app/services/project_service.py (1)
171-175: 💤 Low valueConsider consistent filtering pattern across the codebase.
This code uses splat unpacking to conditionally add a filter:
*([TaskResource.client_origin == client_origin] if client_origin else [])While
operations.pyuses the if-check pattern:if client_origin: query = query.filter(TaskResource.client_origin == client_origin)Both approaches are functionally correct, but using one pattern consistently across the codebase improves readability and maintainability.
🤖 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 `@backend/app/services/project_service.py` around lines 171 - 175, The use of splat/unpacking to conditionally add TaskResource.client_origin == client_origin in the filter is inconsistent with the codebase; replace the expression "*([TaskResource.client_origin == client_origin] if client_origin else [])" with the explicit conditional filter pattern used elsewhere: check if client_origin and then call query = query.filter(TaskResource.client_origin == client_origin) (or equivalent on the QueryBuilder in project_service.py) so filtering is applied only when client_origin is truthy and code style matches operations.py.backend/app/schemas/task.py (1)
107-108: 💤 Low valueConsider making the validation error message more user-friendly.
The error message could list the supported client origins to help API consumers understand what values are valid.
💡 Suggested improvement
if self.client_origin not in SUPPORTED_CLIENT_ORIGINS: - raise ValueError("Unsupported client_origin") + raise ValueError( + f"Unsupported client_origin: {self.client_origin}. " + f"Supported values: {', '.join(SUPPORTED_CLIENT_ORIGINS)}" + )🤖 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 `@backend/app/schemas/task.py` around lines 107 - 108, The ValueError raised in the validation for self.client_origin is too generic; update the check in the Task schema (where self.client_origin is validated against SUPPORTED_CLIENT_ORIGINS) to raise a clearer message that includes the allowed values (e.g., format SUPPORTED_CLIENT_ORIGINS as a comma-separated string or repr and include it in the ValueError) so API consumers see which client_origin values are valid.
🤖 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 `@backend/app/services/adapters/task_kinds/operations.py`:
- Around line 323-327: The stored model_options JSON string is serialized into
labels but never parsed back; modify the retrieval path that builds task dicts
(e.g., convert_to_task_dict_optimized and any other task conversion helpers) to
check for a "modelOptions" label, JSON.parse/deserialise it and populate the
task's model_options field (or modelOptions key) with the resulting dict/object;
ensure you handle missing or invalid JSON by falling back to None/empty dict and
keep the label-based behavior symmetric with the write-side that uses
json_lib.dumps(obj_in.model_options).
In `@backend/app/services/project_device_session_service.py`:
- Around line 37-44: The filter currently uses an explicit boolean comparison
Project.is_active == True; change this to use the truthiness check directly by
replacing that predicate with Project.is_active in the query.filter call (i.e.,
use Project.is_active instead of Project.is_active == True) so the code follows
PEP 8/Black formatting and avoids the explicit True comparison while keeping the
rest of the query (query, Project, project_id, user_id, client_origin)
unchanged.
In `@executor/agents/claude_code/standalone_chat_workspace.py`:
- Around line 79-97: The function prompt_text_for_workspace may append both
"text" and "content" from a single block causing duplicated output; change the
block extraction to use a single prioritized field (e.g., prefer block["text"]
if it's a str, otherwise use block["content"] if it's a str) instead of
appending both. Update the loop over prompt blocks (referencing variables block
and text_parts in prompt_text_for_workspace) to check for "text" first and only
fall back to "content", then append that single chosen string to text_parts
before continuing and join as before.
In `@wework/src/components/chat/composer/ProjectWorkBar.tsx`:
- Line 67: The null-check using loose equality should be changed: update the
isStandaloneMode assignment (symbol: isStandaloneMode, variable:
currentProjectId) to use strict comparisons; replace "currentProjectId == null"
with either "currentProjectId === null" if you only want to detect null, or with
"currentProjectId === null || currentProjectId === undefined" if you intend to
treat undefined the same as null to satisfy TypeScript strict-mode rules.
In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Line 43: Duplicate WEWORK_CLIENT_ORIGIN constants exist; create a single
exported constant (WEWORK_CLIENT_ORIGIN) in a new shared module (e.g.,
api/constants) and replace the local definitions by importing that constant into
WorkbenchProvider and the API modules (projects.ts, tasks.ts); remove the
duplicate local declarations so all uses (including the client_origin
assignments in WorkbenchProvider) reference the single exported symbol.
---
Nitpick comments:
In `@backend/app/models/task.py`:
- Around line 97-103: The Task model's client_origin column lacks a
standalone/indexed column and the existing migration only creates a composite
index (ix_tasks_user_origin_active_project on user_id, client_origin, is_active,
project_id), which won't support queries that filter by client_origin with
project_id/is_active but omit user_id; add an index that matches common query
predicates by either adding index=True to the client_origin Column in the Task
model or (preferably) creating a dedicated Index (e.g., on (client_origin,
is_active, project_id) or (client_origin, is_active)) in the model and adding a
corresponding alembic migration (referencing the Task model's client_origin and
the existing migration file 20260529_e6f7a8b9c012_add_client_origin.py) so the
DB has an index that matches the actual query patterns observed.
In `@backend/app/schemas/task.py`:
- Around line 107-108: The ValueError raised in the validation for
self.client_origin is too generic; update the check in the Task schema (where
self.client_origin is validated against SUPPORTED_CLIENT_ORIGINS) to raise a
clearer message that includes the allowed values (e.g., format
SUPPORTED_CLIENT_ORIGINS as a comma-separated string or repr and include it in
the ValueError) so API consumers see which client_origin values are valid.
In `@backend/app/services/project_service.py`:
- Around line 171-175: The use of splat/unpacking to conditionally add
TaskResource.client_origin == client_origin in the filter is inconsistent with
the codebase; replace the expression "*([TaskResource.client_origin ==
client_origin] if client_origin else [])" with the explicit conditional filter
pattern used elsewhere: check if client_origin and then call query =
query.filter(TaskResource.client_origin == client_origin) (or equivalent on the
QueryBuilder in project_service.py) so filtering is applied only when
client_origin is truthy and code style matches operations.py.
In `@executor/agents/claude_code/standalone_chat_workspace.py`:
- Around line 48-52: The wrapper function slugify_response is redundant because
it simply forwards to slugify_workspace_name; remove slugify_response and update
all callers to call slugify_workspace_name directly (replace any uses of
slugify_response with slugify_workspace_name). Alternatively, if you prefer to
keep the wrapper for API clarity, add a short docstring explaining the
distinction and ensure tests/call sites still use slugify_response
intentionally; otherwise delete slugify_response and run the test suite to
confirm no references remain.
In `@wework/src/api/projects.ts`:
- Around line 13-18: Extract the duplicated WEWORK_CLIENT_ORIGIN constant and
withClientOrigin function into a small shared module (e.g., clientOrigin.ts)
that exports named WEWORK_CLIENT_ORIGIN and withClientOrigin; then remove the
duplicate declaration and inline logic from both projects.ts and tasks.ts and
import { WEWORK_CLIENT_ORIGIN, withClientOrigin } from the new module, making
sure the helper preserves the existing behavior of appending
"client_origin=wework" with the correct '?'/'&' separator.
In `@wework/src/features/workbench/WorkbenchProvider.tsx`:
- Around line 536-581: openTask creates detailTask but the function still uses
`detail as Task` and `detail.subtasks` in places — replace those with the
already-casted `detailTask` to be consistent and readable: use `detailTask` for
the `task` field in the `dispatch({ type: 'task_opened', task: ... })` call and
for the subtasks passed to `sortSubtasksForDisplay(...).map(subtaskToMessage)`
(i.e., replace `detail as Task` and `detail.subtasks` with `detailTask` and
`detailTask.subtasks`), keeping the rest of the logic (standaloneDeviceId logic,
resolvedProjectId handling, and `resolvedServices.chatStream.joinTask(taskId)`)
unchanged.
In `@wework/src/hooks/useEscapeKey.ts`:
- Around line 3-15: The useEscapeKey hook currently re-subscribes the keydown
listener whenever the onEscape callback changes; wrap onEscape with React's
useEffectEvent (import { useEffectEvent } from 'react') inside useEscapeKey to
produce a stable callback used by handleKeyDown, then make the useEffect
dependency list only [enabled] so the listener isn't torn down/recreated on
every onEscape identity change; keep handleKeyDown using the effect
event-wrapped callback and retain the existing add/remove listener logic.
🪄 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: 8c0f0a1e-17f2-4619-af4f-b9be1baad0c4
📒 Files selected for processing (55)
backend/alembic/versions/20260529_e6f7a8b9c012_add_client_origin.pybackend/app/api/endpoints/adapter/tasks.pybackend/app/api/endpoints/projects.pybackend/app/api/ws/chat_namespace.pybackend/app/api/ws/events.pybackend/app/core/constants.pybackend/app/models/project.pybackend/app/models/task.pybackend/app/schemas/project.pybackend/app/schemas/task.pybackend/app/services/adapters/task_kinds/converters.pybackend/app/services/adapters/task_kinds/helpers.pybackend/app/services/adapters/task_kinds/operations.pybackend/app/services/adapters/task_kinds/queries.pybackend/app/services/adapters/task_kinds/query_utils.pybackend/app/services/chat/storage/task_manager.pybackend/app/services/chat/trigger/lifecycle.pybackend/app/services/project_device_session_service.pybackend/app/services/project_service.pybackend/tests/services/adapters/test_task_archive_operations.pyexecutor/agents/claude_code/claude_code_agent.pyexecutor/agents/claude_code/standalone_chat_workspace.pyexecutor/tests/agents/test_project_workspace_paths.pyexecutor/tests/agents/test_standalone_chat_workspace.pywework/src/App.plugins.test.tsxwework/src/api/adapters.test.tswework/src/api/projects.tswework/src/api/tasks.tswework/src/api/users.tswework/src/components/chat/ChatInput.test.tsxwework/src/components/chat/ChatInput.tsxwework/src/components/chat/composer/ComposerToolbar.tsxwework/src/components/chat/composer/ModelSelector.tsxwework/src/components/chat/composer/ProjectChatComposer.tsxwework/src/components/chat/composer/ProjectWorkBar.tsxwework/src/components/common/TextInputDialog.test.tsxwework/src/components/common/TextInputDialog.tsxwework/src/components/layout/DesktopSidebar.tsxwework/src/components/layout/DesktopWorkbenchLayout.test.tsxwework/src/components/layout/DesktopWorkbenchLayout.tsxwework/src/components/layout/MobileWorkbenchLayout.test.tsxwework/src/components/layout/MobileWorkbenchLayout.tsxwework/src/components/projects/ProjectCreateDialog.test.tsxwework/src/components/projects/ProjectCreateDialog.tsxwework/src/features/workbench/WorkbenchProvider.test.tsxwework/src/features/workbench/WorkbenchProvider.tsxwework/src/features/workbench/useWorkbenchModels.tswework/src/features/workbench/workbenchProjectChatHooks.test.tsxwework/src/features/workbench/workbenchReducer.tswework/src/hooks/useEscapeKey.tswework/src/i18n/locales/en/common.jsonwework/src/i18n/locales/zh-CN/common.jsonwework/src/lib/model-ui.tswework/src/pages/WorkbenchPage.tsxwework/src/types/api.ts
| query = db.query(Project).filter( | ||
| Project.id == project_id, | ||
| Project.user_id == user_id, | ||
| Project.is_active == True, | ||
| ) | ||
| if client_origin: | ||
| query = query.filter(Project.client_origin == client_origin) | ||
| project = query.first() |
There was a problem hiding this comment.
Fix the boolean comparison.
Line 40 uses Project.is_active == True which violates PEP 8 style. Use the truthiness check directly instead.
✨ Proposed fix
query = db.query(Project).filter(
Project.id == project_id,
Project.user_id == user_id,
- Project.is_active == True,
+ Project.is_active,
)As per coding guidelines, follow PEP 8, Black formatter (line length: 88), isort, and type hints required for Python.
🧰 Tools
🪛 Ruff (0.15.14)
[error] 40-40: Avoid equality comparisons to True; use Project.is_active: for truth checks
Replace with Project.is_active
(E712)
🤖 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 `@backend/app/services/project_device_session_service.py` around lines 37 - 44,
The filter currently uses an explicit boolean comparison Project.is_active ==
True; change this to use the truthiness check directly by replacing that
predicate with Project.is_active in the query.filter call (i.e., use
Project.is_active instead of Project.is_active == True) so the code follows PEP
8/Black formatting and avoids the explicit True comparison while keeping the
rest of the query (query, Project, project_id, user_id, client_origin)
unchanged.
| def prompt_text_for_workspace(prompt: Any) -> str: | ||
| """Extract user-visible text from an execution prompt.""" | ||
|
|
||
| if isinstance(prompt, str): | ||
| return prompt | ||
| if isinstance(prompt, list): | ||
| text_parts: list[str] = [] | ||
| for block in prompt: | ||
| if not isinstance(block, dict): | ||
| continue | ||
| text = block.get("text") | ||
| if isinstance(text, str): | ||
| text_parts.append(text) | ||
| content = block.get("content") | ||
| if isinstance(content, str): | ||
| text_parts.append(content) | ||
| return "\n".join(text_parts) | ||
| return str(prompt or "") | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Potential double-extraction of text from prompt blocks.
If a prompt block contains both "text" and "content" keys, the function will append both values to text_parts, potentially duplicating content. Consider using an explicit priority (extract "text" OR "content", not both).
♻️ Proposed fix to prioritize one field
if isinstance(prompt, list):
text_parts: list[str] = []
for block in prompt:
if not isinstance(block, dict):
continue
- text = block.get("text")
- if isinstance(text, str):
- text_parts.append(text)
- content = block.get("content")
- if isinstance(content, str):
- text_parts.append(content)
+ # Extract either "text" or "content", with "text" taking precedence
+ text = block.get("text") or block.get("content")
+ if isinstance(text, str):
+ text_parts.append(text)
return "\n".join(text_parts)🤖 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 `@executor/agents/claude_code/standalone_chat_workspace.py` around lines 79 -
97, The function prompt_text_for_workspace may append both "text" and "content"
from a single block causing duplicated output; change the block extraction to
use a single prioritized field (e.g., prefer block["text"] if it's a str,
otherwise use block["content"] if it's a str) instead of appending both. Update
the loop over prompt blocks (referencing variables block and text_parts in
prompt_text_for_workspace) to check for "text" first and only fall back to
"content", then append that single chosen string to text_parts before continuing
and join as before.
| }) | ||
| }, [projects, getDeviceForProject]) | ||
| const standaloneDevices = useMemo(() => sortStandaloneDevices(devices), [devices]) | ||
| const isStandaloneMode = currentProjectId == null |
There was a problem hiding this comment.
Use strict equality operator === instead of ==.
The guideline requires TypeScript strict mode for all TypeScript files. Replace == with === for null checks to maintain type safety and comply with strict mode best practices.
🔧 Proposed fix
- const isStandaloneMode = currentProjectId == null
+ const isStandaloneMode = currentProjectId === null || currentProjectId === undefinedOr more concisely:
- const isStandaloneMode = currentProjectId == null
+ const isStandaloneMode = !currentProjectIdAs per coding guidelines: Use TypeScript strict mode for all TypeScript files
🤖 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 `@wework/src/components/chat/composer/ProjectWorkBar.tsx` at line 67, The
null-check using loose equality should be changed: update the isStandaloneMode
assignment (symbol: isStandaloneMode, variable: currentProjectId) to use strict
comparisons; replace "currentProjectId == null" with either "currentProjectId
=== null" if you only want to detect null, or with "currentProjectId === null ||
currentProjectId === undefined" if you intend to treat undefined the same as
null to satisfy TypeScript strict-mode rules.
| } from './workbenchReducer' | ||
| import { WorkbenchContext } from './useWorkbench' | ||
|
|
||
| const WEWORK_CLIENT_ORIGIN = 'wework' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find all definitions of WEWORK_CLIENT_ORIGIN to ensure single source of truth
rg -n "WEWORK_CLIENT_ORIGIN\s*=" --type=tsRepository: wecode-ai/Wegent
Length of output: 286
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate definitions and see surrounding context / exports
for f in wework/src/api/projects.ts wework/src/api/tasks.ts wework/src/features/workbench/WorkbenchProvider.tsx; do
echo "---- $f (first 80 lines) ----"
sed -n '1,80p' "$f"
done
# Find usages in the repo to see what layer owns the value
rg -n "WEWORK_CLIENT_ORIGIN" --type=ts --type=tsx wework/src | head -n 200
# Check for withClientOrigin usage patterns
rg -n "withClientOrigin" --type=ts --type=tsx wework/src | head -n 200Repository: wecode-ai/Wegent
Length of output: 7695
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="wework/src/features/workbench/WorkbenchProvider.tsx"
echo "== Occurrences of WEWORK_CLIENT_ORIGIN in $file =="
rg -n "WEWORK_CLIENT_ORIGIN" "$file"
echo
echo "== Context around each occurrence (lines 20 before/after) =="
# rg -n gives line numbers; use awk to extract them and show local context via sed
lines=$(rg -n "WEWORK_CLIENT_ORIGIN" "$file" | cut -d: -f1 | sort -n | uniq)
for ln in $lines; do
start=$((ln-20))
end=$((ln+20))
echo "---- $file:$ln ----"
sed -n "${start},${end}p" "$file"
done
echo
echo "== Also check whether WorkbenchProvider calls any API helpers that already embed client_origin =="
rg -n "client_origin|withClientOrigin" "$file"Repository: wecode-ai/Wegent
Length of output: 4709
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for any export/re-export of WEWORK_CLIENT_ORIGIN
rg -n "export (const|{).*WEWORK_CLIENT_ORIGIN|WEWORK_CLIENT_ORIGIN" wework/src/api --type=ts --type=tsx
# Look for shared constants modules
fd -t f -a constants wework/src/api || true
fd -t f -a constants wework/src || true
# Find how chatStream/sendMessage uses client_origin
rg -n "sendMessage\\(|client_origin" wework/src/stream wework/src/features/workbench -S --type=ts --type=tsx | head -n 200
# Check whether createTaskApi or createProjectApi are used for the chat flow
rg -n "createProjectApi\\(|createTaskApi\\(|createProjectApi|createTaskApi" wework/src/features/workbench/WorkbenchProvider.tsxRepository: wecode-ai/Wegent
Length of output: 209
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find all definitions (already known, but ensure no other files)
echo "== Definitions =="
rg -n "const WEWORK_CLIENT_ORIGIN\s*=\s*['\"][^'\"]+['\"]" wework/src --type=ts --type=tsx || true
# 2) Check for exports of WEWORK_CLIENT_ORIGIN
echo
echo "== Exports / re-exports mentioning WEWORK_CLIENT_ORIGIN =="
rg -n "export (const|function|\\{[^}]*\\})|re-export|from .*WEWORK_CLIENT_ORIGIN" wework/src/api wework/src --type=ts --type=tsx || true
# 3) Look for any shared 'client_origin' helper or constants module
echo
echo "== client_origin helper/constants =="
rg -n "withClientOrigin|client_origin" wework/src/api --type=ts --type=tsx || true
# 4) Inspect stream usage (chat payload)
echo
echo "== stream/chatStream sendMessage payload usage =="
rg -n "sendMessage\\(|client_origin" wework/src/stream wework/src/features/workbench --type=ts --type=tsx | head -n 200Repository: wecode-ai/Wegent
Length of output: 357
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# definitions in TS/TSX
echo "== Definitions =="
rg -n "const WEWORK_CLIENT_ORIGIN\s*=\s*['\"][^'\"]+['\"]" wework/src -g'*.ts' -g'*.tsx' || true
echo
echo "== Exports/re-exports involving WEWORK_CLIENT_ORIGIN =="
rg -n "export .*WEWORK_CLIENT_ORIGIN|re-export|from .*WEWORK_CLIENT_ORIGIN" wework/src/api wework/src -g'*.ts' -g'*.tsx' || true
echo
echo "== Search for withClientOrigin helper =="
rg -n "withClientOrigin" wework/src/api -g'*.ts' -g'*.tsx' || true
echo
echo "== Search for chat/task payload usage of client_origin in stream/chatStream =="
# Find sendMessage implementation(s)
rg -n "sendMessage\\s*\\(" wework/src/stream -g'*.ts' -g'*.tsx' || true
# Then inspect the chatStream file(s) likely involved
fd -t f -e ts -e tsx chatStream wework/src/stream wework/src -a || true
fd -t f -n chatStream wework/src/stream wework/src -a || true
# Show relevant portions of any chatStream files
for f in $(fd -a -t f -e ts -e tsx -e js -e jsx "chatStream" wework/src/stream wework/src | head -n 20); do
echo "---- $f (first 200 lines) ----"
sed -n '1,200p' "$f"
doneRepository: wecode-ai/Wegent
Length of output: 13001
Deduplicate WEWORK_CLIENT_ORIGIN into a single exported constant.
WEWORK_CLIENT_ORIGIN is defined separately in wework/src/features/workbench/WorkbenchProvider.tsx (line 43) and duplicated in wework/src/api/projects.ts (line 13) and wework/src/api/tasks.ts (line 11), while WorkbenchProvider uses it for client_origin in the chat/task payloads (lines 751, 792). Centralize it in a shared module (e.g., wework/src/api/constants) and import it in both the API helpers and WorkbenchProvider to avoid drift.
🤖 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 `@wework/src/features/workbench/WorkbenchProvider.tsx` at line 43, Duplicate
WEWORK_CLIENT_ORIGIN constants exist; create a single exported constant
(WEWORK_CLIENT_ORIGIN) in a new shared module (e.g., api/constants) and replace
the local definitions by importing that constant into WorkbenchProvider and the
API modules (projects.ts, tasks.ts); remove the duplicate local declarations so
all uses (including the client_origin assignments in WorkbenchProvider)
reference the single exported symbol.
1dde628 to
ad1e3d5
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
backend/app/services/chat/trigger/lifecycle.py (1)
195-210: 💤 Low valueLog line omits
modelOptions.The metadata update now writes
modelOptions, but the info log at lines 205-210 only reportsmodelIdandautoDeleteExecutor, so persisted model options won't be observable in logs. Optional, but cheap to include.🤖 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 `@backend/app/services/chat/trigger/lifecycle.py` around lines 195 - 210, The info log after updating task metadata omits the newly-set modelOptions; update the logger.info call in prepare_execution_session to include modelOptions (e.g., serialize resolved_task_params.model_options with json.dumps or str) alongside model_id and auto_delete_executor so the persisted modelOptions written to task_crd.metadata.labels["modelOptions"] are visible in logs; reference the existing symbols task_crd, resolved_task_params, and logger.info to locate and modify the log invocation.
🤖 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.
Nitpick comments:
In `@backend/app/services/chat/trigger/lifecycle.py`:
- Around line 195-210: The info log after updating task metadata omits the
newly-set modelOptions; update the logger.info call in prepare_execution_session
to include modelOptions (e.g., serialize resolved_task_params.model_options with
json.dumps or str) alongside model_id and auto_delete_executor so the persisted
modelOptions written to task_crd.metadata.labels["modelOptions"] are visible in
logs; reference the existing symbols task_crd, resolved_task_params, and
logger.info to locate and modify the log invocation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a2256983-c0cf-4b5d-a2d7-b9ef869ccdb5
📒 Files selected for processing (23)
backend/app/api/ws/chat_namespace.pybackend/app/api/ws/events.pybackend/app/schemas/task.pybackend/app/services/adapters/task_kinds/operations.pybackend/app/services/chat/storage/task_manager.pybackend/app/services/chat/trigger/lifecycle.pywework/src/App.plugins.test.tsxwework/src/components/chat/ChatInput.test.tsxwework/src/components/chat/ChatInput.tsxwework/src/components/chat/composer/ComposerToolbar.tsxwework/src/components/chat/composer/ModelSelector.tsxwework/src/components/chat/composer/ProjectChatComposer.tsxwework/src/components/layout/DesktopWorkbenchLayout.test.tsxwework/src/components/layout/MobileWorkbenchLayout.test.tsxwework/src/components/layout/MobileWorkbenchLayout.tsxwework/src/features/workbench/WorkbenchProvider.test.tsxwework/src/features/workbench/WorkbenchProvider.tsxwework/src/features/workbench/useWorkbenchModels.tswework/src/features/workbench/workbenchProjectChatHooks.test.tsxwework/src/i18n/locales/en/common.jsonwework/src/i18n/locales/zh-CN/common.jsonwework/src/lib/model-ui.tswework/src/types/api.ts
✅ Files skipped from review due to trivial changes (1)
- wework/src/features/workbench/workbenchProjectChatHooks.test.tsx
🚧 Files skipped from review as they are similar to previous changes (19)
- wework/src/i18n/locales/en/common.json
- backend/app/api/ws/chat_namespace.py
- wework/src/App.plugins.test.tsx
- wework/src/components/chat/composer/ComposerToolbar.tsx
- wework/src/i18n/locales/zh-CN/common.json
- wework/src/components/layout/MobileWorkbenchLayout.tsx
- wework/src/features/workbench/WorkbenchProvider.test.tsx
- backend/app/schemas/task.py
- wework/src/components/chat/ChatInput.tsx
- wework/src/features/workbench/useWorkbenchModels.ts
- wework/src/lib/model-ui.ts
- wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
- backend/app/services/chat/storage/task_manager.py
- backend/app/api/ws/events.py
- wework/src/types/api.ts
- wework/src/components/chat/ChatInput.test.tsx
- wework/src/components/chat/composer/ModelSelector.tsx
- wework/src/features/workbench/WorkbenchProvider.tsx
- backend/app/services/adapters/task_kinds/operations.py
ad1e3d5 to
c8663b7
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
wework/src/components/chat/composer/ModelSelector.tsx (1)
26-237: ⚖️ Poor tradeoffConsider extracting the menu content into subcomponents.
The
ModelSelectorcomponent body is ~210 lines, well above the preferred per-function limit. The two-pane menu (family sidebar + right panel) andrenderControlSectionare good candidates to extract into smaller presentational components to improve readability and testability. As per coding guidelines, "Function length: Max 50 lines per function (preferred)".🤖 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 `@wework/src/components/chat/composer/ModelSelector.tsx` around lines 26 - 237, The ModelSelector component is too large; extract the menu parts into smaller presentational components: create a ModelSelectorMenu (rendering the outer menu container and grid), a FamilyList component (rendering the left column using familyGroups, activeGroup, activeFamilyId and handlers like setActiveFamilyId), a ModelList component (rendering the right column using activeGroup, selectedModel, onSelectModel and controlsBelowModels), and a ControlSection component to replace renderControlSection (accepting a ModelControlConfig and onSelectModelOption, selectedModelOptions). Move related helpers/props (controlsAboveFamilies, controlsBelowModels, selectedModelControls) into ModelSelector and pass only needed data/callbacks to these new components to keep ModelSelector focused and under the function-length guideline.
🤖 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 `@wework/src/components/chat/composer/ModelSelector.tsx`:
- Around line 147-149: Hover-driven changes to activeFamilyId via the
onMouseEnter handler cause activeControlModel to toggle and recompute
controlsAboveFamilies (which for the 'gpt' family adds a family-scoped control),
producing layout shifts and hover jitter; remove the onMouseEnter call that sets
activeFamilyId in ModelSelector.tsx and retain only onFocus and onClick to
change activeFamilyId, or alternatively move rendering of controlsAboveFamilies
to a non-shifting region (e.g., right panel) so toggling family-specific
controls (from wework/src/lib/model-ui.ts) does not shift the family list.
- Around line 214-223: The setOpen updater in ModelSelector currently performs a
side effect by calling setActiveFamilyId inside the state updater; remove that
impure behavior by computing the nextOpen value outside the updater and invoking
setActiveFamilyId only when nextOpen is true (and only if activeFamilyId is not
already set) before calling setOpen(nextOpen). Specifically, change the onClick
handler to determine const nextOpen = !open (or use the current value read from
state), call setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ??
'') if opening and activeFamilyId is empty, then call setOpen(nextOpen); this
avoids running setActiveFamilyId inside the setOpen(state => ...) updater and
prevents duplicate runs that cause displayedFamilyId → activeGroup →
controlsAboveFamilies layout jitter when hovering.
---
Nitpick comments:
In `@wework/src/components/chat/composer/ModelSelector.tsx`:
- Around line 26-237: The ModelSelector component is too large; extract the menu
parts into smaller presentational components: create a ModelSelectorMenu
(rendering the outer menu container and grid), a FamilyList component (rendering
the left column using familyGroups, activeGroup, activeFamilyId and handlers
like setActiveFamilyId), a ModelList component (rendering the right column using
activeGroup, selectedModel, onSelectModel and controlsBelowModels), and a
ControlSection component to replace renderControlSection (accepting a
ModelControlConfig and onSelectModelOption, selectedModelOptions). Move related
helpers/props (controlsAboveFamilies, controlsBelowModels,
selectedModelControls) into ModelSelector and pass only needed data/callbacks to
these new components to keep ModelSelector focused and under the function-length
guideline.
🪄 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: 59642e5d-b98a-4389-92e6-33a532101ab5
📒 Files selected for processing (23)
backend/app/api/ws/chat_namespace.pybackend/app/api/ws/events.pybackend/app/schemas/task.pybackend/app/services/adapters/task_kinds/operations.pybackend/app/services/chat/storage/task_manager.pybackend/app/services/chat/trigger/lifecycle.pywework/src/App.plugins.test.tsxwework/src/components/chat/ChatInput.test.tsxwework/src/components/chat/ChatInput.tsxwework/src/components/chat/composer/ComposerToolbar.tsxwework/src/components/chat/composer/ModelSelector.tsxwework/src/components/chat/composer/ProjectChatComposer.tsxwework/src/components/layout/DesktopWorkbenchLayout.test.tsxwework/src/components/layout/MobileWorkbenchLayout.test.tsxwework/src/components/layout/MobileWorkbenchLayout.tsxwework/src/features/workbench/WorkbenchProvider.test.tsxwework/src/features/workbench/WorkbenchProvider.tsxwework/src/features/workbench/useWorkbenchModels.tswework/src/features/workbench/workbenchProjectChatHooks.test.tsxwework/src/i18n/locales/en/common.jsonwework/src/i18n/locales/zh-CN/common.jsonwework/src/lib/model-ui.tswework/src/types/api.ts
✅ Files skipped from review due to trivial changes (2)
- wework/src/i18n/locales/zh-CN/common.json
- wework/src/App.plugins.test.tsx
🚧 Files skipped from review as they are similar to previous changes (18)
- wework/src/i18n/locales/en/common.json
- backend/app/api/ws/chat_namespace.py
- wework/src/components/layout/MobileWorkbenchLayout.test.tsx
- wework/src/components/layout/MobileWorkbenchLayout.tsx
- backend/app/services/chat/trigger/lifecycle.py
- wework/src/types/api.ts
- wework/src/components/chat/composer/ProjectChatComposer.tsx
- wework/src/components/chat/ChatInput.tsx
- wework/src/lib/model-ui.ts
- wework/src/features/workbench/WorkbenchProvider.test.tsx
- backend/app/services/chat/storage/task_manager.py
- backend/app/schemas/task.py
- wework/src/features/workbench/useWorkbenchModels.ts
- wework/src/components/chat/ChatInput.test.tsx
- wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
- backend/app/api/ws/events.py
- backend/app/services/adapters/task_kinds/operations.py
- wework/src/features/workbench/WorkbenchProvider.tsx
| onMouseEnter={() => setActiveFamilyId(group.config.id)} | ||
| onFocus={() => setActiveFamilyId(group.config.id)} | ||
| onClick={() => setActiveFamilyId(group.config.id)} |
There was a problem hiding this comment.
Hover-driven activeFamilyId changes can shift the family sidebar
onMouseEnter/onFocus/onClick all set activeFamilyId, which changes activeControlModel and therefore recomputes controlsAboveFamilies (rendered above the family list). In wework/src/lib/model-ui.ts, only the gpt family adds a family-scoped control (reasoning with scope: 'family'); claude, gemini, and kimi have controls: []. Switching families on hover can show/hide that above-controls section (length toggling), shifting the family buttons and potentially causing hover jitter. Consider removing onMouseEnter (keep onFocus/onClick) or moving the controls to a stable layout region (e.g., right panel).
🤖 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 `@wework/src/components/chat/composer/ModelSelector.tsx` around lines 147 -
149, Hover-driven changes to activeFamilyId via the onMouseEnter handler cause
activeControlModel to toggle and recompute controlsAboveFamilies (which for the
'gpt' family adds a family-scoped control), producing layout shifts and hover
jitter; remove the onMouseEnter call that sets activeFamilyId in
ModelSelector.tsx and retain only onFocus and onClick to change activeFamilyId,
or alternatively move rendering of controlsAboveFamilies to a non-shifting
region (e.g., right panel) so toggling family-specific controls (from
wework/src/lib/model-ui.ts) does not shift the family list.
| onClick={() => { | ||
| if (disabled) return | ||
| setOpen(current => { | ||
| const nextOpen = !current | ||
| if (nextOpen) { | ||
| setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ?? '') | ||
| } | ||
| return nextOpen | ||
| }) | ||
| }} |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
React 19 must state updater functions passed to setState be pure?
💡 Result:
Yes, updater functions passed to state setters (such as those returned by useState) must be pure [1]. In React 19, as in previous versions, updater functions run during the rendering process [1]. Because React may invoke these functions multiple times—for instance, to calculate state transitions within a batch or during Strict Mode development checks—they must be pure to ensure predictable behavior [1]. Key requirements for updater functions include: - Pureness: They should only calculate and return the next state based on the previous state provided as an argument [1]. - No Side Effects: They must not perform side effects, such as modifying other variables, triggering network requests, or calling other state setters [1]. React enforces this behavior during development in Strict Mode by invoking updater functions twice; the first result is discarded, and the second is used, which helps expose non-pure logic or unintended side effects [1]. Failing to keep these functions pure can lead to bugs, inconsistent UI states, or unnecessary re-renders [1][2].
Citations:
- 1: https://react.dev/learn/queueing-a-series-of-state-updates
- 2: https://dev.to/a1guy/mastering-usestate-react-state-deep-dive-basics-gotchas-patterns-2cli
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="wework/src/components/chat/composer/ModelSelector.tsx"
# Print the relevant section with line numbers for context (around 214-223).
nl -ba "$FILE" | sed -n '180,250p'Repository: wecode-ai/Wegent
Length of output: 103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="wework/src/components/chat/composer/ModelSelector.tsx"
# Show a wider window around the reported lines, with line numbers
cat -n "$FILE" | sed -n '190,245p'Repository: wecode-ai/Wegent
Length of output: 2071
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="wework/src/components/chat/composer/ModelSelector.tsx"
# 1) Locate open/setOpen + activeFamilyId definitions
rg -n "const \\[open\\]|setOpen\\b|aria-expanded=\\{open\\}" "$FILE"
rg -n "setActiveFamilyId\\b|activeFamilyId\\b" "$FILE"
# 2) Look for hover/focus/click handlers that setActiveFamilyId
rg -n "(onMouseEnter|onMouseLeave|onPointerEnter|onPointerLeave|onFocus|onBlur|onClick).*setActiveFamilyId|setActiveFamilyId.*(onMouseEnter|onMouseLeave|onPointerEnter|onPointerLeave|onFocus|onBlur|onClick)" "$FILE" || trueRepository: wecode-ai/Wegent
Length of output: 999
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="wework/src/components/chat/composer/ModelSelector.tsx"
# Inspect derived state / filtering logic around where activeFamilyId is used (near the earlier line numbers)
cat -n "$FILE" | sed -n '60,190p'Repository: wecode-ai/Wegent
Length of output: 6836
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="wework/src/components/chat/composer/ModelSelector.tsx"
wc -l "$FILE"
echo "---- top of file ----"
cat -n "$FILE" | sed -n '1,90p'
echo "---- renderControlSection function length (rough) ----"
# Print the full function renderControlSection block for line range context
cat -n "$FILE" | sed -n '70,140p'Repository: wecode-ai/Wegent
Length of output: 7521
Avoid side effects inside the setOpen state updater (and reduce menu jitter on hover)
- In
wework/src/components/chat/composer/ModelSelector.tsx(lines ~214-223), callingsetActiveFamilyId(...)insidesetOpen(current => ...)makes the updater impure; React may invoke updater functions multiple times in development, so this can run twice and cause inconsistent state. activeFamilyIdis also updated ononMouseEnter/onFocusfor family buttons; sinceactiveFamilyIdfeedsdisplayedFamilyId→activeGroup→controlsAboveFamilies, this can change the left column’s height and visually jitter while hovering near the family list.
♻️ Proposed refactor
- onClick={() => {
- if (disabled) return
- setOpen(current => {
- const nextOpen = !current
- if (nextOpen) {
- setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ?? '')
- }
- return nextOpen
- })
- }}
+ onClick={() => {
+ if (disabled) return
+ const nextOpen = !open
+ if (nextOpen) {
+ setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ?? '')
+ }
+ setOpen(nextOpen)
+ }}📝 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.
| onClick={() => { | |
| if (disabled) return | |
| setOpen(current => { | |
| const nextOpen = !current | |
| if (nextOpen) { | |
| setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ?? '') | |
| } | |
| return nextOpen | |
| }) | |
| }} | |
| onClick={() => { | |
| if (disabled) return | |
| const nextOpen = !open | |
| if (nextOpen) { | |
| setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ?? '') | |
| } | |
| setOpen(nextOpen) | |
| }} |
🤖 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 `@wework/src/components/chat/composer/ModelSelector.tsx` around lines 214 -
223, The setOpen updater in ModelSelector currently performs a side effect by
calling setActiveFamilyId inside the state updater; remove that impure behavior
by computing the nextOpen value outside the updater and invoking
setActiveFamilyId only when nextOpen is true (and only if activeFamilyId is not
already set) before calling setOpen(nextOpen). Specifically, change the onClick
handler to determine const nextOpen = !open (or use the current value read from
state), call setActiveFamilyId(selectedFamily ?? familyGroups[0]?.config.id ??
'') if opening and activeFamilyId is empty, then call setOpen(nextOpen); this
avoids running setActiveFamilyId inside the setOpen(state => ...) updater and
prevents duplicate runs that cause displayedFamilyId → activeGroup →
controlsAboveFamilies layout jitter when hovering.
c8663b7 to
a2f7a9a
Compare
1ba316e to
0ad0529
Compare
0ad0529 to
9536876
Compare
Summary
Tests
Integration
Note: DingTalk notification intentionally skipped per request.