Skip to content

feat: 看板体验优化 - #2346

Merged
icycrystal4 merged 9 commits into
wecode-ai:mainfrom
Micro66:human/eagle-20260729-055424
Jul 31, 2026
Merged

feat: 看板体验优化#2346
icycrystal4 merged 9 commits into
wecode-ai:mainfrom
Micro66:human/eagle-20260729-055424

Conversation

@Micro66

@Micro66 Micro66 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added customizable project boards with configurable statuses, grouping, card fields, filtering, and search.
    • Added project and task archiving, including child-task handling.
    • Added project-specific chat with conversation restoration, resizing, and task context.
    • Added AI Table views, view creation, filtering, sorting, and improved grid controls.
    • Added richer task details, including assignees, due dates, source data, and flexible statuses.
    • Added improved DingTalk AI Table integration and project configuration controls.
  • Bug Fixes
    • Improved authentication, task synchronization, archive behavior, and Windows background-process handling.
  • Documentation
    • Added English and Simplified Chinese translations for new project and chat features.

Micro66 added 6 commits July 29, 2026 17:56
…055424

# Conflicts:
#	wework/scripts/prepare-dws-binary.mjs
#	wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
#	wework/src/components/layout/workspace-panels/TemporaryChatPanel.tsx
#	wework/src/i18n/locales/en/common.json
#	wework/src/i18n/locales/zh-CN/common.json
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds configurable cloud-project boards, custom statuses, recursive archival, AITable views and field mapping, DingTalk runtime routing, project-scoped chat, and updated desktop chat behavior.

Changes

Cloud board and runtime integration

Layer / File(s) Summary
Backend board contracts and lifecycle
backend/app/schemas/*, backend/app/services/*, backend/app/api/endpoints/*, backend/tests/api/*
Adds board configuration, custom status validation, assignee names, project and loop-item archival, status cleanup, and API coverage.
Local runtime operations and IPC
executor/src/task_runtime/*, executor/src/local/app_ipc.rs, executor/src/agents/*, executor/tests/*
Adds versioned project and recursive task archival, board metadata persistence, archive IPC routes, skill resolution, and contract coverage.
AITable views and provider routing
executor/src/task_runtime/aitable_provider.rs, executor/src/task_runtime/mcp.rs, executor/src/task_runtime/aitable_provider_tests.rs
Adds view creation and filtering, inferred field mappings, enriched user cells, date normalization, and DingTalk-specific redirects.
Frontend delivery adapters
wework/src/api/*, wework/src/api/local/*
Exposes board metadata, AITable views, enriched task data, project archival, and loop-item archival.
Board management and workspace UI
wework/src/features/todo/*
Adds AG Grid rendering, board layout editing, configurable cards, grouping, filtering, project management, and archive actions.
Project chat and runtime context
wework/src/features/todo/*, wework/src/components/layout/*, wework/src/features/workbench/*
Adds project conversations, task-linked drafts, runtime context, lifecycle-aware sending, shared chat layout, and DingTalk skill propagation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes, which improve the cloud project board experience and related task workflows.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
wework/src/features/todo/AITableView.tsx (1)

409-439: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The mount effect never sets loading back to true.

loading starts as true, so the first run is correct. This effect also re-runs when api or project changes. On a re-run, loading is already false, so the grid keeps rendering the previous project's records and view until the new request resolves. Set setLoading(true) at the start of the effect, as load already does.

🐛 Proposed fix
   useEffect(() => {
     let cancelled = false
     ;(async () => {
+      setLoading(true)
       setError(null)
       try {
🤖 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/todo/AITableView.tsx` around lines 409 - 439, Update the
mount effect in AITableView’s useEffect to call setLoading(true) at the
beginning of each run, before resetting the error and starting the async
request, so api or project changes show the loading state while new data is
fetched.
🟡 Minor comments (19)
wework/src/features/todo/ProjectSpaceChatSidebar.tsx-106-115 (1)

106-115: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move the draft copy into the i18n namespaces.

taskConversationDraft hardcodes Chinese user-visible text ([$任务:...], 任务描述:, 补充说明:). The rest of this component resolves copy through t(). Add these strings to src/i18n/locales/en/common.json and src/i18n/locales/zh-CN/common.json, then pass t into the helper.

♻️ Proposed change
 function taskConversationDraft(
   projectId: string | number,
-  request: ProjectSpaceChatLaunchRequest | null
+  request: ProjectSpaceChatLaunchRequest | null,
+  t: (key: string, options?: Record<string, unknown>) => string
 ): string {
   if (!request) return ''
-  const reference = `[$任务:${request.item.id}](cloud://projects/${projectId}/todos/${request.item.id})`
+  const reference = `[$${t('workbench.project_space_chat.task_mention_label')}:${request.item.id}](cloud://projects/${projectId}/todos/${request.item.id})`
   return request.item.description.trim()
-    ? `${reference}\n\n任务描述:\n${request.item.description.trim()}\n\n补充说明:\n`
+    ? `${reference}\n\n${t('workbench.project_space_chat.task_description_label')}\n${request.item.description.trim()}\n\n${t('workbench.project_space_chat.task_notes_label')}\n`
     : `${reference}\n\n`
 }

As per coding guidelines: "Add new copy to the appropriate Wework namespace in both src/i18n/locales/en/ and src/i18n/locales/zh-CN/".

🤖 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/todo/ProjectSpaceChatSidebar.tsx` around lines 106 - 115,
Update taskConversationDraft to accept the component’s t translator and replace
all hardcoded user-visible text with i18n keys. Add the corresponding English
and Simplified Chinese strings to common.json in both locale namespaces, then
pass t from the component when calling the helper while preserving the existing
draft formatting and conditional description behavior.

Source: Coding guidelines

wework/src/features/todo/TodoEditor.tsx-721-723 (1)

721-723: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the configured status color for the status dot.

columnDotClasses only contains the five built-in statuses. When project.board_config.statuses supplies custom statuses, every dot renders bg-zinc-400. todoShared.ts adds boardStatusColorClasses for this case, and CloudTodoWorkspace.tsx already uses it. Resolve the color from the selected option first.

♻️ Proposed change
+              {/* resolve the dot color from the configured status, then fall back */}
               <span
-                className={cn('h-2 w-2 rounded-full', columnDotClasses[status] ?? 'bg-zinc-400')}
+                className={cn(
+                  'h-2 w-2 rounded-full',
+                  boardStatusColorClasses[
+                    statusOptions.find(option => option.id === status)?.color ?? ''
+                  ] ??
+                    columnDotClasses[status] ??
+                    'bg-zinc-400'
+                )}
               />

Import boardStatusColorClasses from ./todoShared.

🤖 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/todo/TodoEditor.tsx` around lines 721 - 723, Update the
status-dot rendering in TodoEditor to import and use boardStatusColorClasses
from todoShared, resolving the selected status option’s configured color before
falling back to columnDotClasses and then the existing default class. Ensure
custom project.board_config.statuses colors are preserved.
wework/src/i18n/locales/en/common.json-1991-1995 (1)

1991-1995: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one public-project access scope in both locales.

The public-project description says all WeWork users can view a project. The management description says only users connected to the current Backend can view it. These descriptions must state the same authorization boundary.

  • wework/src/i18n/locales/en/common.json#L1991-L1995: Update project_visibility_manage_description to match the intended public-project audience.
  • wework/src/i18n/locales/zh-CN/common.json#L1990-L1994: Update project_visibility_manage_description with the same audience as the English string.
🤖 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/i18n/locales/en/common.json` around lines 1991 - 1995, Align
project_visibility_manage_description in wework/src/i18n/locales/en/common.json
(lines 1991-1995) and wework/src/i18n/locales/zh-CN/common.json (lines
1990-1994) so both describe the same intended public-project audience as
project_visibility_public_description, removing the conflicting current-Backend
restriction while preserving each locale’s language.
backend/app/services/loop_items/service.py-587-603 (1)

587-603: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the descendant traversal against parent cycles.

The loop re-queries children of each newly found level and never tracks visited identifiers. If any stored parent_id chain forms a cycle, the loop never terminates and archived_items grows without bound. _validate_parent_change blocks new cycles, but it does not protect rows written before that validation existed or rows written by other paths.

Track visited identifiers and exclude them from the next query.

🛡️ Proposed fix
         pending_parent_ids = [item.id]
         archived_items = [item]
+        visited = {item.id}
         while pending_parent_ids:
             children = (
                 db.query(LoopItem)
                 .filter(
                     LoopItem.cloud_project_id == item.cloud_project_id,
                     LoopItem.parent_id.in_(pending_parent_ids),
                     loop_datetime_is_unset(LoopItem.deleted_at),
                 )
                 .all()
             )
-            pending_parent_ids = [child.id for child in children]
-            archived_items.extend(children)
+            children = [child for child in children if child.id not in visited]
+            visited.update(child.id for child in children)
+            pending_parent_ids = [child.id for child in children]
+            archived_items.extend(children)
🤖 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/loop_items/service.py` around lines 587 - 603, Update
the descendant traversal in the archival logic to track visited LoopItem
identifiers, initialize it with the starting item, and exclude already visited
IDs from each children query. Add newly discovered child IDs to the visited set
before continuing, while preserving the existing archived_items collection and
deletion/version updates.
executor/src/agents/claude_code.rs-675-686 (1)

675-686: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use the real skill metadata for placeholder SkillRefs.

resolve_skill returns only (skill_id, namespace), but the deployed skill path uses the placeholder "is_public": false and "content_hash": None. This is safe for most task skills, but a public skill with a cached manifest will only remain cached while a different backend route and hash state keep the manifest up-to-date; otherwise, every successful deploy still looks like a fresh cache miss. Pass is_public/content_hash from the skill metadata instead of hard-coding the defaults.

🤖 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/src/agents/claude_code.rs` around lines 675 - 686, Update the
successful resolve_skill handling to obtain and use the resolved skill’s actual
metadata, including is_public and content_hash, when constructing the SkillRef
inserted into plan.resolved_skill_map. Extend the resolved result or reuse the
available skill metadata so these fields are no longer hard-coded to false and
None, while preserving the existing skill_id and namespace values.
wework/src/features/todo/CloudTodoWorkspace.tsx-914-922 (1)

914-922: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Priority column labels do not match the priority labels on the cards.

Line 917 renders 普通 for none and the raw value for every other priority, so the columns read low, medium, high, and urgent. CloudTodoBoardCard.tsx lines 16-22 render the same values as , , , and 紧急. The board therefore shows two different labels for one priority. Export the label map from CloudTodoBoardCard.tsx and reuse it here.

🤖 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/todo/CloudTodoWorkspace.tsx` around lines 914 - 922,
Update the priority column construction around nativeGroupBy to reuse the
exported label map from CloudTodoBoardCard.tsx for every priority, including
none, instead of the inline 普通/raw-value labels. Import and apply that shared
map while preserving the existing priority keys and metadata.
wework/src/features/todo/CloudTodoWorkspace.tsx-1077-1080 (1)

1077-1080: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

An unavailable project-space API fails without user feedback. All three handlers resolve the API through apiForProjectId and then abandon the operation, so the dialog stays open with no message. The rename handler additionally throws outside its try block; because line 2389 calls it as void renameSelectedProject(), that throw becomes an unhandled promise rejection and never reaches setRenameError. Set the corresponding error state at each site instead.

  • wework/src/features/todo/CloudTodoWorkspace.tsx#L1077-L1080: replace throw new Error('项目空间接口当前不可用') with setRenameError(...) followed by return.
  • wework/src/features/todo/CloudTodoWorkspace.tsx#L1101-L1104: replace the bare if (!api) return with setArchiveError(...) followed by return.
  • wework/src/features/todo/CloudTodoWorkspace.tsx#L1124-L1127: replace the bare if (!api) return with setArchiveError(...) followed by return.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx` around lines 1077 - 1080,
Update renameSelectedProject at
wework/src/features/todo/CloudTodoWorkspace.tsx#L1077-L1080 to setRenameError
and return when apiForProjectId returns no API, rather than throwing. Update the
handlers at `#L1101-L1104` and `#L1124-L1127` to setArchiveError and return instead
of silently returning, so all unavailable project-space API cases provide user
feedback.
wework/src/features/todo/CloudTodoBoardCard.tsx-160-196 (1)

160-196: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two new popup surfaces lack complete dismissal handling. Both open an overlay from a trigger button, and neither implements the full dismissal pair that CloudTodoWorkspace.tsx lines 805-826 already establish for the project menu. Keyboard users cannot close either overlay.

  • wework/src/features/todo/CloudTodoBoardCard.tsx#L160-L196: add an outside-click handler and an Escape handler for menuOpen, and add role="menu" plus role="menuitem".
  • wework/src/features/todo/CloudTodoWorkspace.tsx#L138-L145: extend the existing mousedown effect in AITableGroupFieldPicker with a keydown handler that closes the picker on Escape.
🤖 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/todo/CloudTodoBoardCard.tsx` around lines 160 - 196, The
CloudTodoBoardCard menu lacks complete dismissal and accessibility behavior: add
outside-click and Escape handling tied to menuOpen, mark its container as
role="menu", and its archive button as role="menuitem". In
wework/src/features/todo/CloudTodoWorkspace.tsx lines 138-145, extend the
AITableGroupFieldPicker mousedown effect with a keydown listener that closes the
picker on Escape.
wework/src/features/todo/CloudTodoWorkspace.tsx-905-913 (1)

905-913: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-status columns inherit status: 'inbox', so every empty column shows the inbox hint.

The AITable, priority, assignee, and tag columns all set status to 'inbox'. Line 2197 then looks up columnEmptyHints[column.status] and renders the inbox hint inside every empty column, including a DingTalk group column and the 无标签 column. Key the empty hint on the grouping mode, or only look it up when nativeGroupBy === 'status'.

🐛 Proposed fix
-                                  {columnItems.length === 0 && columnEmptyHints[column.status] && (
+                                  {columnItems.length === 0 &&
+                                    !isAITableProject &&
+                                    nativeGroupBy === 'status' &&
+                                    columnEmptyHints[column.status] && (
                                     <div className="rounded-xl border border-dashed border-border px-3 py-6 text-center text-xs text-text-muted">
                                       {columnEmptyHints[column.status]}
                                     </div>
                                   )}

Also applies to: 953-971

🤖 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/todo/CloudTodoWorkspace.tsx` around lines 905 - 913,
Update the empty-column rendering near the columnEmptyHints lookup and the
boardColumns definitions for AITable, priority, assignee, and tag grouping so
non-status columns do not use their inherited status value to select an inbox
hint. Only resolve and render columnEmptyHints when nativeGroupBy is 'status',
or otherwise key the hint by the active grouping mode, while preserving
status-based hints for actual status columns.
wework/src/features/todo/CloudTodoBoardCard.tsx-78-83 (1)

78-83: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The date badge shows updated_at when no due date exists.

Line 81 falls back from item.due_at to item.updated_at, and both render behind the same CalendarDays icon with the same MM-DD slice. A reader cannot tell a deadline from a last-modified date. Show the date only when due_at is set, or label the fallback so the two meanings stay distinct.

🐛 Proposed fix to distinguish the two dates
-          {display.showDate ? (
-            <span className="inline-flex items-center gap-1">
+          {display.showDate ? (
+            <span
+              className="inline-flex items-center gap-1"
+              title={item.due_at ? '截止日期' : '最近更新'}
+            >
               <CalendarDays className="h-3 w-3" />
               {(item.due_at ?? item.updated_at).slice(5, 10)}
             </span>
           ) : null}
🤖 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/todo/CloudTodoBoardCard.tsx` around lines 78 - 83, Update
the date badge in the component’s showDate rendering to avoid presenting
updated_at as a due date: render the CalendarDays date only when item.due_at is
set, or explicitly label any updated_at fallback so its meaning is distinct.
Preserve the existing MM-DD formatting for due dates.
wework/src/features/todo/CloudTodoWorkspace.tsx-1066-1075 (1)

1066-1075: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

selectProject does not reset the AITable filter and search state.

The function clears nativeGroupFilter and nativeBoardQuery, but leaves aitableGroupFilter and aitableBoardQuery untouched. The column predicate at lines 2139-2143 applies both values without an isAITableProject guard. After a user filters a DingTalk board and switches to another project, every column filters against the stale value and the board renders empty. Reset the AITable state in the same function.

🐛 Proposed fix
   function selectProject(projectId: string | null) {
     setSelectedProjectId(projectId)
     setProjectView('board')
     setBoardParentId(null)
     setNativeGroupFilter('')
     setNativeBoardQuery('')
+    setAitableGroupFilter('')
+    setAitableBoardQuery('')
     setProjectSearchOpen(false)
     setProjectSearchQuery('')
     setProjectSearchFilters(emptyTaskSearchFilters)
   }
🤖 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/todo/CloudTodoWorkspace.tsx` around lines 1066 - 1075,
Update selectProject to also reset the AITable filter and search state by
clearing aitableGroupFilter and aitableBoardQuery alongside the existing native
filter resets, so switching projects cannot retain stale AITable values.
wework/src/features/todo/AITableView.tsx-134-142 (1)

134-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Date normalization uses UTC, while the grid value uses local time.

Line 137 converts a millisecond timestamp with toISOString().slice(0, 10), which yields the UTC calendar date. gridValue at line 122 builds a Date that AG Grid sorts and filters in local time. For a timestamp that falls on a different calendar day in UTC than in the user's timezone, the displayed and edited date differs from the sorted and filtered date by one day. Derive the date from local components so both paths agree.

🐛 Proposed fix to use local date components
 function editorText(field: AITableField, value: unknown): string {
   if (field.type !== 'date') return cellText(value)
   if (typeof value === 'number' && Number.isFinite(value)) {
-    return new Date(value).toISOString().slice(0, 10)
+    const date = new Date(value)
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+    return `${date.getFullYear()}-${month}-${day}`
   }
   const text = cellText(value)
   const match = text.match(/^\d{4}-\d{2}-\d{2}/)
   return match?.[0] ?? 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 `@wework/src/features/todo/AITableView.tsx` around lines 134 - 142, Update
editorText for numeric date values to derive the YYYY-MM-DD string from the
Date’s local year, month, and day components instead of toISOString().slice(0,
10). Keep the existing handling for non-numeric values unchanged so displayed
and edited dates match gridValue’s local-time behavior.
wework/src/features/todo/CloudTodoWorkspace.tsx-2022-2037 (1)

2022-2037: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reading localStorage during render makes the button state impure.

Line 2022 calls localStorage.getItem(personalGroupKey) inside the render body. React cannot re-render when that external value changes, so the 应用到全局 button appears and disappears only when some other state change happens to trigger a render. savePersonalGroupBy and saveGlobalGroupBy currently force that render as a side effect, which makes the behavior incidental. Track the override in component state and keep localStorage writes in the handlers.

♻️ Proposed fix to move the flag into state
+  const [hasPersonalGroupOverride, setHasPersonalGroupOverride] = useState(false)
   function savePersonalGroupBy(groupBy: NativeBoardGroupBy) {
     setNativeGroupBy(groupBy)
     setNativeGroupFilter('')
-    if (personalGroupKey) localStorage.setItem(personalGroupKey, groupBy)
+    if (personalGroupKey) {
+      localStorage.setItem(personalGroupKey, groupBy)
+      setHasPersonalGroupOverride(true)
+    }
   }
-      if (personalGroupKey) localStorage.removeItem(personalGroupKey)
+      if (personalGroupKey) {
+        localStorage.removeItem(personalGroupKey)
+        setHasPersonalGroupOverride(false)
+      }
-                      {personalGroupKey && localStorage.getItem(personalGroupKey) ? (
+                      {personalGroupKey && hasPersonalGroupOverride ? (

Set the initial value inside the existing effect at lines 872-885, which already reads the same key.

🤖 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/todo/CloudTodoWorkspace.tsx` around lines 2022 - 2037,
Replace the render-time localStorage.getItem check in CloudTodoWorkspace with
component state for the personal-group override flag. Initialize or synchronize
that state in the existing effect that reads personalGroupKey, and update it
explicitly in savePersonalGroupBy and saveGlobalGroupBy after their localStorage
writes so rendering depends only on React state.
wework/src/features/todo/BoardLayoutEditor.tsx-216-226 (1)

216-226: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Date.now() alone can produce duplicate status ids.

Two addStatus calls inside the same millisecond generate the same id. That id is the dnd-kit sortable id, the React key, and the persisted status id, so duplicates break sorting and rename or delete targeting. Derive the id from a value that cannot repeat, for example crypto.randomUUID(), or reject an id that already exists in statuses.

🤖 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/todo/BoardLayoutEditor.tsx` around lines 216 - 226,
Update addStatus so each new status receives an id that cannot collide with
existing statuses, replacing the Date.now()-based generation with
crypto.randomUUID() or an equivalent uniqueness check against statuses. Preserve
the existing status creation and onStatusesChange behavior.
executor/src/task_runtime/mcp.rs-531-542 (1)

531-542: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The redirect can emit null ids while instructing the agent to use them.

project.metadata["provider_config"]["base_id"] returns Value::Null when the project has no stored binding, so the payload can contain "base_id": null together with "Use these bound IDs directly". The fallback branch only covers a missing project, not a project without base_id or table_id.

Treat a missing base_id or table_id as unbound and return the fallback instruction instead.

🤖 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/src/task_runtime/mcp.rs` around lines 531 - 542, Update the project
mapping around the redirect payload to validate that provider_config contains
non-null base_id and table_id before emitting the bound-resource instruction.
For projects missing either binding, return the existing fallback instruction
instead; only include the direct-use payload when both IDs are present, while
preserving the optional view_id behavior.
wework/src/features/todo/CloudProjectManageView.tsx-658-687 (1)

658-687: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

aitableBusy drives two unrelated buttons.

waitForDwsAuthentication polls for up to 120 seconds (160 attempts × 750 ms). During that wait, aitableBusy also disables "保存连接" at Line 681, and the connect button shows "等待浏览器授权…" while a plain save is in progress. Use a separate flag for the DingTalk login wait.

The polling loop also continues after unmount, because nothing cancels it. Add a cancellation flag or an AbortSignal so the view stops polling when the user leaves the page.

🤖 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/todo/CloudProjectManageView.tsx` around lines 658 - 687,
Separate the DingTalk login-wait state from the save state: update the handlers
and button rendering around waitForDwsAuthentication so only the connect button
uses a dedicated login-wait flag, while the save button uses its existing
save-busy state. Add cancellation tied to the view lifecycle for
waitForDwsAuthentication, using an AbortSignal or equivalent cleanup flag, and
stop the polling promptly when the component unmounts or the login flow is
cancelled.
executor/src/task_runtime/store.rs-297-302 (1)

297-302: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use project board_config.statuses when validating local tasks.

update_project stores any board_config, but create_task, update_task, and reorder_tasks call validate_status against the fixed list "inbox" | "pending" | "in_progress" | "in_review" | "completed". This rejects task status values valid for a custom local board. Pass the project status ids to these local validation paths or add project-aware validation.

🤖 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/src/task_runtime/store.rs` around lines 297 - 302, Update the local
task validation used by create_task, update_task, and reorder_tasks to load the
project’s board_config.statuses and validate against those status IDs instead of
the fixed default list. Ensure update_project’s stored board_config is reused
for project-aware validation while preserving the existing default statuses when
no custom board configuration exists.
wework/src/features/todo/BoardLayoutEditor.tsx-198-205 (1)

198-205: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Close the display menu on Escape.

The menu closes only on outside pointerdown. A keyboard user who opens cloud-board-display-menu cannot dismiss it without a pointer, and focus is not returned to the trigger. Add an Escape handler in the same effect.

♿ Proposed fix
   useEffect(() => {
     if (!fieldsOpen) return
     const close = (event: PointerEvent) => {
       if (!fieldsRef.current?.contains(event.target as Node)) setFieldsOpen(false)
     }
+    const closeOnEscape = (event: KeyboardEvent) => {
+      if (event.key === 'Escape') setFieldsOpen(false)
+    }
     document.addEventListener('pointerdown', close)
-    return () => document.removeEventListener('pointerdown', close)
+    document.addEventListener('keydown', closeOnEscape)
+    return () => {
+      document.removeEventListener('pointerdown', close)
+      document.removeEventListener('keydown', closeOnEscape)
+    }
   }, [fieldsOpen])
🤖 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/todo/BoardLayoutEditor.tsx` around lines 198 - 205,
Update the useEffect managing fieldsOpen and the fieldsRef outside-click handler
to also listen for keydown events and close the display menu when Escape is
pressed. Remove the keydown listener during cleanup, and return focus to the
menu trigger after closing.
executor/src/task_runtime/store.rs-329-343 (1)

329-343: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinguish unknown or already archived projects from stale archives.

archive_project maps every zero-row update to VersionConflict, so an archive call for a missing loop_items project id or an already archived project also sends version_conflict; this can surface to the desktop as a false “project changed” result. Map the 0-update case to ProjectNotFound, and clear deleted_at via list_projects()/get_project() into account for the already archived case before returning VersionConflict for a stale version.

🤖 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/src/task_runtime/store.rs` around lines 329 - 343, Update
archive_project so a zero-row update first checks list_projects()/get_project()
for the project and its deleted_at state: return ProjectNotFound when the
project is missing or already archived, and return VersionConflict only when an
existing active project has a stale version. Preserve the successful archive
path and version update behavior.
🤖 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/loop_items/service.py`:
- Around line 123-125: Update the list response paths that repeatedly call
loop_item_service.response_values(), especially the LoopItemListResponse and
MyWorkListResponse construction in deliveries.py, to batch-load or eager-load
all assignee users before mapping items. Refactor response_values() and its
callers to reuse the preloaded user data instead of executing db.get(User,
item.assignee_user_id) for each item, while preserving assignee_name behavior
for missing or unassigned users.

In `@executor/src/task_runtime/aitable_provider.rs`:
- Around line 501-579: The enrich_user_cells method should resolve user
identifiers through a bounded concurrent stream rather than awaiting each
contact user search sequentially, while preserving the existing per-request
limit behavior as appropriate. Add provider-level caching keyed by user
identifier, reuse cached names across calls, and ensure identifiers beyond the
current 30-item processing limit are not silently left unresolved; update the
name-enrichment pass to use cached or newly resolved results.
- Around line 768-807: Update infer_board_mapping so title candidates are
evaluated in declared priority order, preferring exact field-name matches before
substring matches, and exclude fields whose IDs are already assigned to other
mapping keys. Apply this selection specifically to title_field_id while
preserving the existing fallback to the first field when no suitable title
remains.

In `@executor/src/task_runtime/store.rs`:
- Around line 504-528: Update archive_task to accept the target task version,
start a TransactionBehavior::Immediate transaction before the task check, and
perform both get_task validation and the recursive archive update through that
transaction. Add the matching version predicate to the target-row update, return
VersionConflict when no row is updated due to a version mismatch, and commit the
transaction on success. Update the todos.archive IPC route to receive and
forward the version argument.

In `@wework/scripts/prepare-dws-binary.mjs`:
- Around line 53-66: Update extractZip to resolve the destination and each
archive entry path, then reject entries whose resolved output is outside the
destination boundary before creating directories or writing files. Use the
node:path separator as needed to enforce a proper descendant check while
allowing paths within the destination.

In `@wework/src/api/deliveries.ts`:
- Line 56: Replace hard-coded status switch mappings in DesktopWorkbenchMain and
other CloudLoopItem status consumers with lookups against
project.board_config.statuses, using the matching custom status id to resolve
its label. Preserve existing labels for built-in statuses and provide the
established fallback behavior when no configured status matches.

In `@wework/src/components/layout/workspace-panels/TemporaryChatPanel.tsx`:
- Around line 217-219: Update both lifecycleStore.syncTranscript calls in
TemporaryChatPanel to set preserveActiveTurn from
lifecycleStore.getTask(address)?.derived.isTurnActive, rather than
derived.isRunning, so submitting, awaiting, and running turns remain preserved
during transcript synchronization.

In `@wework/src/features/todo/CloudProjectManageView.tsx`:
- Around line 390-393: Replace hardcoded user-facing copy with the local
useTranslation('common') wrapper: in
wework/src/features/todo/CloudProjectManageView.tsx:390-393 and throughout the
view, restore translation usage for headings, controls, prompts, and error
fallbacks; in wework/src/features/todo/BoardLayoutEditor.tsx:243-248 and
throughout the editor, translate headings, helper text, menu labels,
aria-labels, and the default 新状态 name. Add matching keys to both
wework/src/i18n/locales/en/ and wework/src/i18n/locales/zh-CN/, register any new
namespace in wework/src/i18n/index.ts, and update BoardLayoutEditor.test.tsx to
assert translated output rather than raw copy.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx`:
- Around line 82-201: Split CloudTodoWorkspace into cohesive modules to keep it
under the 1000-line guideline: move AITableGroupFieldPicker into its own module
following CloudTodoBoardCard.tsx’s pattern, move nativeBoardGroupFields,
nativeBoardStatusColors, and aitableCellLabels into todoShared.ts, and extract
the boardColumns construction into a memoized helper module while preserving its
existing behavior and interfaces.
- Around line 1328-1344: Update the tag-handling branches in the item move
logic, including the mapping callback and the corresponding code around the
alternate path, so dragging between tag columns replaces only the tag owned by
the current grouping while preserving all other tags. Treat an empty
column.groupValue as removing only the grouping tag rather than clearing
candidate.tags, and leave priority and assignee behavior unchanged.
- Around line 2223-2231: Update the DragOverlay rendering in CloudTodoWorkspace
to resolve the active item from items and render CloudTodoCardContent only when
that lookup returns an item; remove the non-null assertion from items.find(...).
Preserve the existing overlay styling and null rendering when there is no active
drag item or the item has disappeared.

In `@wework/src/features/todo/TodoEditor.tsx`:
- Around line 733-739: Update the status select rendering in TodoEditor so that
when the current status is non-empty and absent from statusOptions, it adds an
option for that status, while preserving the existing placeholder and mapped
options. Use the current status value and a suitable label, and ensure normal
statuses already present in statusOptions are not duplicated.

---

Outside diff comments:
In `@wework/src/features/todo/AITableView.tsx`:
- Around line 409-439: Update the mount effect in AITableView’s useEffect to
call setLoading(true) at the beginning of each run, before resetting the error
and starting the async request, so api or project changes show the loading state
while new data is fetched.

---

Minor comments:
In `@backend/app/services/loop_items/service.py`:
- Around line 587-603: Update the descendant traversal in the archival logic to
track visited LoopItem identifiers, initialize it with the starting item, and
exclude already visited IDs from each children query. Add newly discovered child
IDs to the visited set before continuing, while preserving the existing
archived_items collection and deletion/version updates.

In `@executor/src/agents/claude_code.rs`:
- Around line 675-686: Update the successful resolve_skill handling to obtain
and use the resolved skill’s actual metadata, including is_public and
content_hash, when constructing the SkillRef inserted into
plan.resolved_skill_map. Extend the resolved result or reuse the available skill
metadata so these fields are no longer hard-coded to false and None, while
preserving the existing skill_id and namespace values.

In `@executor/src/task_runtime/mcp.rs`:
- Around line 531-542: Update the project mapping around the redirect payload to
validate that provider_config contains non-null base_id and table_id before
emitting the bound-resource instruction. For projects missing either binding,
return the existing fallback instruction instead; only include the direct-use
payload when both IDs are present, while preserving the optional view_id
behavior.

In `@executor/src/task_runtime/store.rs`:
- Around line 297-302: Update the local task validation used by create_task,
update_task, and reorder_tasks to load the project’s board_config.statuses and
validate against those status IDs instead of the fixed default list. Ensure
update_project’s stored board_config is reused for project-aware validation
while preserving the existing default statuses when no custom board
configuration exists.
- Around line 329-343: Update archive_project so a zero-row update first checks
list_projects()/get_project() for the project and its deleted_at state: return
ProjectNotFound when the project is missing or already archived, and return
VersionConflict only when an existing active project has a stale version.
Preserve the successful archive path and version update behavior.

In `@wework/src/features/todo/AITableView.tsx`:
- Around line 134-142: Update editorText for numeric date values to derive the
YYYY-MM-DD string from the Date’s local year, month, and day components instead
of toISOString().slice(0, 10). Keep the existing handling for non-numeric values
unchanged so displayed and edited dates match gridValue’s local-time behavior.

In `@wework/src/features/todo/BoardLayoutEditor.tsx`:
- Around line 216-226: Update addStatus so each new status receives an id that
cannot collide with existing statuses, replacing the Date.now()-based generation
with crypto.randomUUID() or an equivalent uniqueness check against statuses.
Preserve the existing status creation and onStatusesChange behavior.
- Around line 198-205: Update the useEffect managing fieldsOpen and the
fieldsRef outside-click handler to also listen for keydown events and close the
display menu when Escape is pressed. Remove the keydown listener during cleanup,
and return focus to the menu trigger after closing.

In `@wework/src/features/todo/CloudProjectManageView.tsx`:
- Around line 658-687: Separate the DingTalk login-wait state from the save
state: update the handlers and button rendering around waitForDwsAuthentication
so only the connect button uses a dedicated login-wait flag, while the save
button uses its existing save-busy state. Add cancellation tied to the view
lifecycle for waitForDwsAuthentication, using an AbortSignal or equivalent
cleanup flag, and stop the polling promptly when the component unmounts or the
login flow is cancelled.

In `@wework/src/features/todo/CloudTodoBoardCard.tsx`:
- Around line 160-196: The CloudTodoBoardCard menu lacks complete dismissal and
accessibility behavior: add outside-click and Escape handling tied to menuOpen,
mark its container as role="menu", and its archive button as role="menuitem". In
wework/src/features/todo/CloudTodoWorkspace.tsx lines 138-145, extend the
AITableGroupFieldPicker mousedown effect with a keydown listener that closes the
picker on Escape.
- Around line 78-83: Update the date badge in the component’s showDate rendering
to avoid presenting updated_at as a due date: render the CalendarDays date only
when item.due_at is set, or explicitly label any updated_at fallback so its
meaning is distinct. Preserve the existing MM-DD formatting for due dates.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx`:
- Around line 914-922: Update the priority column construction around
nativeGroupBy to reuse the exported label map from CloudTodoBoardCard.tsx for
every priority, including none, instead of the inline 普通/raw-value labels.
Import and apply that shared map while preserving the existing priority keys and
metadata.
- Around line 1077-1080: Update renameSelectedProject at
wework/src/features/todo/CloudTodoWorkspace.tsx#L1077-L1080 to setRenameError
and return when apiForProjectId returns no API, rather than throwing. Update the
handlers at `#L1101-L1104` and `#L1124-L1127` to setArchiveError and return instead
of silently returning, so all unavailable project-space API cases provide user
feedback.
- Around line 905-913: Update the empty-column rendering near the
columnEmptyHints lookup and the boardColumns definitions for AITable, priority,
assignee, and tag grouping so non-status columns do not use their inherited
status value to select an inbox hint. Only resolve and render columnEmptyHints
when nativeGroupBy is 'status', or otherwise key the hint by the active grouping
mode, while preserving status-based hints for actual status columns.
- Around line 1066-1075: Update selectProject to also reset the AITable filter
and search state by clearing aitableGroupFilter and aitableBoardQuery alongside
the existing native filter resets, so switching projects cannot retain stale
AITable values.
- Around line 2022-2037: Replace the render-time localStorage.getItem check in
CloudTodoWorkspace with component state for the personal-group override flag.
Initialize or synchronize that state in the existing effect that reads
personalGroupKey, and update it explicitly in savePersonalGroupBy and
saveGlobalGroupBy after their localStorage writes so rendering depends only on
React state.

In `@wework/src/features/todo/ProjectSpaceChatSidebar.tsx`:
- Around line 106-115: Update taskConversationDraft to accept the component’s t
translator and replace all hardcoded user-visible text with i18n keys. Add the
corresponding English and Simplified Chinese strings to common.json in both
locale namespaces, then pass t from the component when calling the helper while
preserving the existing draft formatting and conditional description behavior.

In `@wework/src/features/todo/TodoEditor.tsx`:
- Around line 721-723: Update the status-dot rendering in TodoEditor to import
and use boardStatusColorClasses from todoShared, resolving the selected status
option’s configured color before falling back to columnDotClasses and then the
existing default class. Ensure custom project.board_config.statuses colors are
preserved.

In `@wework/src/i18n/locales/en/common.json`:
- Around line 1991-1995: Align project_visibility_manage_description in
wework/src/i18n/locales/en/common.json (lines 1991-1995) and
wework/src/i18n/locales/zh-CN/common.json (lines 1990-1994) so both describe the
same intended public-project audience as project_visibility_public_description,
removing the conflicting current-Backend restriction while preserving each
locale’s language.

---

Nitpick comments:
In `@backend/app/services/loop_items/service.py`:
- Around line 52-66: Update _project_status_ids to import and reuse the
default_board_statuses helper from the cloud project schema when board statuses
are missing or invalid, returning the helper’s status identifiers instead of
duplicating the hardcoded fallback list.

In `@executor/src/agents/claude_code.rs`:
- Around line 670-699: Replace the sequential resolution loop around
resolve_skill with a bounded concurrent stream using
skill_download_concurrency() and buffer_unordered. Collect each skill’s
resolution result, then sequentially update plan.resolved_skill_map and emit the
existing not-found or failure events, preserving already-resolved skill skipping
and per-skill error handling.

In `@executor/src/local/app_ipc.rs`:
- Around line 715-725: Update the "projects.archive" handler to obtain the
version through the existing required_task_i64 helper, matching the files.move
pattern, and remove the duplicated inline parameter parsing while preserving the
existing archive_project call and error propagation.

In `@executor/src/task_runtime/aitable_provider.rs`:
- Around line 57-72: Update the detached login process in the task runtime
provider to retain stderr instead of suppressing it, then have the tokio::spawn
task await the child and log both the exit status and captured stderr. Preserve
the existing successful response and spawn-error handling while ensuring
failures after spawn are recorded.
- Around line 477-499: Cache the table schema and view configuration for the
duration of each board operation to avoid repeated CLI lookups. Update
board_config and the create_board/update_board/get_board/list_board call chain
to reuse one field result per base/table, and update the view lookup in
list_records to reuse one view configuration across all paginated requests when
view_id is supplied. Keep cache scope limited to the current board operation and
preserve existing mapping and lookup behavior.

In `@executor/src/task_runtime/mcp.rs`:
- Around line 505-512: Remove the unused _tool_name parameter from
is_locally_routed_project and update every production and test call site to pass
only runtime and project_id. Do not add a compatibility wrapper or retain the
dead argument.
- Around line 145-153: Update call_tool to resolve the requested project’s
LoopItem once per invocation, then pass that resolved project to
is_dingtalk_aitable_project, is_locally_routed_project, and
dingtalk_route_redirect instead of letting each helper reload the project list.
Apply the same reuse pattern in visible_tools so each tool-list request performs
only one project lookup.

In `@executor/src/task_runtime/model.rs`:
- Around line 64-65: Replace the arbitrary serde_json::Value types for
board_config and card_display with typed serde-deserializable structs matching
CloudProjectBoardConfig and its status entries. Enforce unique status
identifiers, the identifier pattern, valid color values, and a maximum of 50
statuses at IPC deserialization so LocalTaskStore::update_project only receives
valid configurations.

In `@executor/src/task_runtime/store.rs`:
- Around line 108-117: The default board statuses are duplicated between the
configuration in the store initialization and backend’s
default_board_statuses(); consolidate them through one shared definition, or add
a test asserting the status IDs, names, and colors remain identical across both
paths.

In `@executor/tests/local_app_ipc_contract.rs`:
- Around line 255-265: Add a stale-version dispatch for projects.archive before
the existing successful archive in the test, using an outdated version and
asserting the returned conflict code is "version_conflict". Keep the
current-project version for the subsequent successful archive and preserve the
final empty-project assertion.

In `@wework/src/api/aitable.ts`:
- Around line 67-70: Remove the unused createView method from the AITable
interface and delete its local implementation, including related dead code. Keep
the AITable contract minimal and preserve the existing behavior where kanban
creation is unavailable and view tabs remain hidden.

In `@wework/src/features/todo/AITableView.tsx`:
- Line 335: Replace the hardcoded user-visible strings `删除字段`, `删除记录`, and
`暂无记录` in `AITableView` with keys from the local `@/hooks/useTranslation`
wrapper, and add matching English and Chinese entries to the appropriate Wework
locale namespace under `src/i18n/locales/en/` and `src/i18n/locales/zh-CN/`.
- Around line 13-50: Replace AllCommunityModule registration with imports and
registration of only the AG Grid modules required by the features used in
AITableView and its AgGridReact configuration. Update
ModuleRegistry.registerModules accordingly, while preserving the existing grid
behavior and aitableGridTheme setup.

In `@wework/src/features/todo/BoardLayoutEditor.test.tsx`:
- Around line 33-34: Remove the literal sample-text assertion from the
BoardLayoutEditor test. Replace the getByText check for “补充项目管理页的空状态” with an
assertion targeting the preview container or a stable test id, while keeping the
existing layout-settings assertion unchanged.
- Around line 42-62: Extend the BoardLayoutEditor tests with a render using
canEditStatuses={false}, then assert that the cloud-board-status-add element is
absent. Keep the existing editable-status test unchanged and reuse the current
fixture props and query utilities.

In `@wework/src/features/todo/BoardLayoutEditor.tsx`:
- Around line 311-336: In BoardLayoutEditor, extract the preview-column
calculation from the statuses.map render into a named previewIndex constant
above the map, preserving the second-column-or-first-when-only-one behavior, and
use previewIndex for the SortableStatus previewDisplay condition.

In `@wework/src/features/todo/CloudProjectManageView.tsx`:
- Around line 294-341: Refresh the items from the server in the catch blocks of
both renameTag and deleteTag after any partial update failure, before clearing
the busy state, so local item tags and versions match completed server updates.
Keep the existing error reporting and success-path persistence behavior
unchanged.
- Around line 170-175: Remove the explicit version arguments from the
updateProject call sites around the affected handlers, while leaving
updateProject’s internal merge of the current version unchanged. Ensure each
caller passes only its update values so updateProject remains the single source
of truth for version.

In `@wework/src/features/todo/CloudTodoBoardCard.tsx`:
- Around line 16-22: Move all user-visible strings in CloudTodoBoardCard,
including priorityLabels and the labels referenced in the indicated ranges, into
the appropriate Wework translation namespace in both English and zh-CN locale
files. Use the local `@/hooks/useTranslation` wrapper within CloudTodoBoardCard to
read these translation keys, preserving the current Chinese text as zh-CN values
and adding corresponding English translations.

In `@wework/src/features/todo/CloudTodoWorkspace.test.tsx`:
- Around line 204-259: Add an automated E2E scenario covering project archival,
task archival, and launching project chat through the real application flow,
rather than the mocked component paths in the tests around the project and task
archival cases. Update the GitHub Actions workflow to invoke this scenario and
verify it runs as part of CI.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx`:
- Around line 737-790: Update the useLayoutEffect measurement logic around
compute so it no longer depends on projectHeaderLevel, since that value is set
by the effect. Change setProjectHeaderLevel to use its previous state and return
the previous level when next is unchanged, preventing redundant commits;
preserve the existing level calculation and ResizeObserver behavior.
- Around line 1382-1394: Extract the shared column-membership predicate into an
itemMatchesColumn(item, column) helper, covering AITable and all native grouping
modes consistently. Replace the inline targetColumn predicate in finishBoardDrop
and the duplicated filter logic around the column rendering path with this
helper, preserving each caller’s existing behavior while ensuring both paths use
the same grouping rules.
- Around line 1592-1630: Replace the listed literal Chinese UI strings in
CloudTodoWorkspace, including the project action buttons and the additional
affected sections, with keys resolved through `@/hooks/useTranslation`. Add the
corresponding English and zh-CN entries to the appropriate Wework locale
namespace, and register that namespace in src/i18n/index.ts if it is new;
preserve the existing todo namespace usage and provide equivalent translations
for every new key.

In `@wework/src/features/todo/projectProviderConfig.test.ts`:
- Around line 78-86: Add a test alongside the existing
dingtalkAITableRuntimeContext cases covering a DingTalk project whose
provider_config omits base_id and table_id or contains blank values, and assert
the function returns undefined. Keep the existing non-DingTalk provider test
unchanged.

In `@wework/src/features/todo/projectProviderConfig.ts`:
- Around line 123-132: Move the static rules array out of the function
containing dingtalkAITableRuntimeContext into a module-level constant, then have
the function reuse that constant. Preserve the existing rule text and runtime
behavior while reducing per-call array allocation and keeping the function under
the 50-line guideline.

In `@wework/src/features/todo/ProjectSpaceChatSidebar.tsx`:
- Around line 63-66: Update runtimeCloudProjectId to read only
task.runtimeHandle?.cloudProjectId, removing the cloud_project_id fallback while
preserving the existing string/number normalization and null behavior.
- Around line 203-212: Update startNewConversation to read creatingNew directly
instead of performing side effects inside the setCreatingNew updater. Return
early when creatingNew is already true; otherwise reset selectedConversationKey,
draft, and draftItemId, increment chatInstance, and then set creatingNew to
true, keeping the updater pure or removing it.
🪄 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 Plus

Run ID: 21ee4d5e-e622-44c6-b193-1622f2158729

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc238c and ee0db87.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (51)
  • backend/app/api/endpoints/cloud_projects.py
  • backend/app/api/endpoints/deliveries.py
  • backend/app/schemas/cloud_project.py
  • backend/app/schemas/delivery.py
  • backend/app/services/cloud_projects/service.py
  • backend/app/services/loop_items/service.py
  • backend/tests/api/test_cloud_projects_api.py
  • executor/src/agents/claude_code.rs
  • executor/src/agents/runtime_capabilities.rs
  • executor/src/local/app_ipc.rs
  • executor/src/task_runtime/aitable_provider.rs
  • executor/src/task_runtime/aitable_provider_tests.rs
  • executor/src/task_runtime/mcp.rs
  • executor/src/task_runtime/model.rs
  • executor/src/task_runtime/router.rs
  • executor/src/task_runtime/store.rs
  • executor/tests/local_app_ipc_contract.rs
  • pnpm-workspace.yaml
  • wework/package.json
  • wework/scripts/prepare-dws-binary.mjs
  • wework/src-tauri/src/system_sleep.rs
  • wework/src/api/aitable.ts
  • wework/src/api/deliveries.ts
  • wework/src/api/dws.ts
  • wework/src/api/local/localDelivery.test.ts
  • wework/src/api/local/localDelivery.ts
  • wework/src/api/local/localServices.test.ts
  • wework/src/api/local/localServices.ts
  • wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/components/layout/desktopChatLayout.ts
  • wework/src/components/layout/workspace-panels/TemporaryChatPanel.tsx
  • wework/src/features/todo/AITableView.test.tsx
  • wework/src/features/todo/AITableView.tsx
  • wework/src/features/todo/BoardLayoutEditor.test.tsx
  • wework/src/features/todo/BoardLayoutEditor.tsx
  • wework/src/features/todo/CloudProjectManageView.tsx
  • wework/src/features/todo/CloudTodoBoardCard.tsx
  • wework/src/features/todo/CloudTodoWorkspace.test.tsx
  • wework/src/features/todo/CloudTodoWorkspace.tsx
  • wework/src/features/todo/ProjectSpaceChatSidebar.test.tsx
  • wework/src/features/todo/ProjectSpaceChatSidebar.tsx
  • wework/src/features/todo/TodoEditor.tsx
  • wework/src/features/todo/projectProviderConfig.test.ts
  • wework/src/features/todo/projectProviderConfig.ts
  • wework/src/features/todo/todoShared.ts
  • wework/src/features/workbench/useWorkbenchRuntimeMessaging.ts
  • wework/src/features/workbench/workbenchContextTypes.ts
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
💤 Files with no reviewable changes (1)
  • wework/src/components/layout/DesktopWorkbenchLayout.tsx

Comment on lines +123 to +125
if item.assignee_user_id:
assignee = db.get(User, item.assignee_user_id)
values["assignee_name"] = assignee.user_name if assignee else None

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every caller of response_values and check for per-item loops.
set -euo pipefail

rg -n -C8 'response_values' --glob '*.py'

Repository: wecode-ai/Wegent

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf 'Tracked Python paths under backend/app/services/loop_items/service.py:\n'
git ls-files 'backend/app/services/loop_items/service.py' 'backend/**/*.py' | sed -n '1,120p'
printf '\nFile exists/stat:\n'
if [ -f backend/app/services/loop_items/service.py ]; then
  wc -l backend/app/services/loop_items/service.py
  ast-grep outline backend/app/services/loop_items/service.py --view expanded || true
fi

Repository: wecode-ai/Wegent

Length of output: 8595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'LoopItemService relevant sections:\n'
cat -n backend/app/services/loop_items/service.py | sed -n '1,340p'

printf '\nSearch for response_values in tracked Python files:\n'
rg -n -C6 'response_values|list_my_work|\\.response_values|list\\(' --glob '*.py' backend/app | sed -n '1,240p'

Repository: wecode-ai/Wegent

Length of output: 14395


🏁 Script executed:

#!/bin/bash
set -xeu

printf 'Remaining LoopItemService relevant section:\n'
cat -n backend/app/services/loop_items/service.py | sed -n '340,900p'

printf '\nSearch for response_values / list_my_work / loop_item_service in tracked Python files:\n'
rg -n -C6 'response_values|list_my_work|loop_item_service|service\.list\(|service\.get\(' --glob '*.py' backend/app

Repository: wecode-ai/Wegent

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Response schema definitions:\n'
rg -n -C5 'class (LoopItemResponse|LoopItemListResponse|MyWorkItemResponse|MyWorkListResponse)|assignee_name|assignee_user_id' backend/app/schemas --glob '*.py'

Repository: wecode-ai/Wegent

Length of output: 5803


Batch assignee lookups on list response paths.

backend/app/api/endpoints/deliveries.py builds LoopItemListResponse and MyWorkListResponse by calling loop_item_service.response_values() for each item. Since response_values() runs one db.get(User, item.assignee_user_id) per task, use a batch user lookup or eager-load assignees before building repeated item responses.

🤖 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/loop_items/service.py` around lines 123 - 125, Update
the list response paths that repeatedly call
loop_item_service.response_values(), especially the LoopItemListResponse and
MyWorkListResponse construction in deliveries.py, to batch-load or eager-load
all assignee users before mapping items. Refactor response_values() and its
callers to reuse the preloaded user data instead of executing db.get(User,
item.assignee_user_id) for each item, while preserving assignee_name behavior
for missing or unassigned users.

Comment on lines +501 to +579
async fn enrich_user_cells(&self, fields: &[Value], records: &mut [Value]) {
let user_fields = fields
.iter()
.filter(|field| field.get("type").and_then(Value::as_str) == Some("user"))
.filter_map(|field| field.get("id").and_then(Value::as_str))
.collect::<Vec<_>>();
let mut names = HashMap::new();
let mut user_ids = Vec::new();
for record in records.iter() {
for field_id in &user_fields {
let Some(users) = record
.get("cells")
.and_then(|cells| cells.get(*field_id))
.and_then(Value::as_array)
else {
continue;
};
for user_id in users.iter().filter_map(|user| {
user.get("userId")
.or_else(|| user.get("user_id"))
.and_then(Value::as_str)
}) {
if !user_ids.iter().any(|candidate| candidate == user_id) {
user_ids.push(user_id.to_owned());
}
}
}
}
for user_id in user_ids.into_iter().take(30) {
let Ok(response) = self
.run(&["contact", "user", "search", "--query", &user_id])
.await
else {
continue;
};
let user = list_from(&response, &["result", "users", "items", "data"])
.into_iter()
.find(|user| {
user.get("userId")
.or_else(|| user.get("user_id"))
.and_then(Value::as_str)
== Some(user_id.as_str())
});
if let Some(name) = user.as_ref().and_then(|user| {
user.get("name")
.or_else(|| user.get("nick"))
.and_then(Value::as_str)
}) {
names.insert(user_id, name.to_owned());
}
}
for record in records {
for field_id in &user_fields {
let Some(users) = record
.get_mut("cells")
.and_then(|cells| cells.get_mut(*field_id))
.and_then(Value::as_array_mut)
else {
continue;
};
for user in users {
let Some(object) = user.as_object_mut() else {
continue;
};
let user_id = object
.get("userId")
.or_else(|| object.get("user_id"))
.and_then(Value::as_str)
.map(ToOwned::to_owned);
if let Some(user_id) = user_id {
object.insert(
"name".to_owned(),
json!(names.get(&user_id).cloned().unwrap_or(user_id)),
);
}
}
}
}
}

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Reduce the per-user process spawns in enrich_user_cells.

The loop awaits contact user search once per distinct user identifier, in sequence. Each run call spawns a DWS CLI process. A board with 30 assignees therefore launches 30 processes in series on every list_board call, and get_board calls list_board. No result is cached between calls.

Two changes limit the cost:

  • Resolve the identifiers with a bounded concurrent stream instead of a sequential loop.
  • Cache resolved names on the provider, keyed by user identifier.

Also note that take(30) silently drops the remaining identifiers, so those cells keep the raw user identifier as the displayed name.

🤖 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/src/task_runtime/aitable_provider.rs` around lines 501 - 579, The
enrich_user_cells method should resolve user identifiers through a bounded
concurrent stream rather than awaiting each contact user search sequentially,
while preserving the existing per-request limit behavior as appropriate. Add
provider-level caching keyed by user identifier, reuse cached names across
calls, and ensure identifiers beyond the current 30-item processing limit are
not silently left unresolved; update the name-enrichment pass to use cached or
newly resolved results.

Comment on lines +768 to +807
fn infer_board_mapping(mapping: &mut Map<String, Value>, fields: &[Value]) {
let candidates = [
("title_field_id", &["标题", "任务名称", "任务", "名称"][..]),
("description_field_id", &["描述", "备注", "详情"][..]),
("status_field_id", &["状态", "进度"][..]),
("parent_field_id", &["父记录", "父任务"][..]),
("priority_field_id", &["优先级"][..]),
("assignee_field_id", &["负责人", "执行人"][..]),
(
"due_field_id",
&["截止时间", "计划结束日期", "截止日期"][..],
),
];
for (key, names) in candidates {
if mapping_get(mapping, key).is_some() {
continue;
}
let field = fields.iter().find(|field| {
field
.get("name")
.and_then(Value::as_str)
.is_some_and(|name| names.iter().any(|candidate| name.contains(candidate)))
});
if let Some(field_id) = field
.and_then(|field| field.get("id"))
.and_then(Value::as_str)
{
mapping.insert(key.to_owned(), json!(field_id));
}
}
if mapping_get(mapping, "title_field_id").is_none() {
if let Some(field_id) = fields
.first()
.and_then(|field| field.get("id"))
.and_then(Value::as_str)
{
mapping.insert("title_field_id".to_owned(), json!(field_id));
}
}
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The title inference can select the wrong field.

The closure accepts a field when its name contains any candidate string. The title candidates include "任务". That substring also appears in "父任务" and "任务状态". fields.iter().find returns the first field in schema order, so a board whose parent or status field precedes the real title field maps title_field_id to that field. The title then renders the wrong cell, and create_board writes the title into the wrong column.

Match the candidates in priority order, prefer an exact name match, and exclude fields already assigned to another mapping key.

🛠️ Proposed direction
+    let mut assigned = std::collections::HashSet::new();
     for (key, names) in candidates {
         if mapping_get(mapping, key).is_some() {
+            if let Some(id) = mapping_get(mapping, key) {
+                assigned.insert(id.to_owned());
+            }
             continue;
         }
-        let field = fields.iter().find(|field| {
-            field
-                .get("name")
-                .and_then(Value::as_str)
-                .is_some_and(|name| names.iter().any(|candidate| name.contains(candidate)))
-        });
+        // Prefer an exact match, then fall back to a substring match, and
+        // never reuse a field that another mapping key already claims.
+        let matches = |exact: bool| {
+            names.iter().find_map(|candidate| {
+                fields.iter().find(|field| {
+                    let Some(name) = field.get("name").and_then(Value::as_str) else {
+                        return false;
+                    };
+                    let id = field.get("id").and_then(Value::as_str).unwrap_or_default();
+                    !assigned.contains(id)
+                        && if exact { name == *candidate } else { name.contains(candidate) }
+                })
+            })
+        };
+        let field = matches(true).or_else(|| matches(false));
         if let Some(field_id) = field
             .and_then(|field| field.get("id"))
             .and_then(Value::as_str)
         {
             mapping.insert(key.to_owned(), json!(field_id));
+            assigned.insert(field_id.to_owned());
         }
     }
🤖 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/src/task_runtime/aitable_provider.rs` around lines 768 - 807, Update
infer_board_mapping so title candidates are evaluated in declared priority
order, preferring exact field-name matches before substring matches, and exclude
fields whose IDs are already assigned to other mapping keys. Apply this
selection specifically to title_field_id while preserving the existing fallback
to the first field when no suitable title remains.

Comment on lines +504 to +528
pub fn archive_task(&self, project_id: &str, task_id: &str) -> Result<(), TaskRuntimeError> {
self.get_task(project_id, task_id)?;
let connection = self.connection()?;
let archived_at = now();
let updated = connection.execute(
"WITH RECURSIVE task_tree(id) AS (
SELECT id FROM loop_items
WHERE id = ?1 AND resource_type = 'task'
AND cloud_project_id = ?2 AND deleted_at IS NULL
UNION ALL
SELECT child.id FROM loop_items child
JOIN task_tree parent ON child.parent_id = parent.id
WHERE child.resource_type = 'task'
AND child.cloud_project_id = ?2 AND child.deleted_at IS NULL
)
UPDATE loop_items
SET deleted_at = ?3, updated_at = ?3, version = version + 1
WHERE id IN (SELECT id FROM task_tree)",
params![task_id, project_id, archived_at],
)?;
if updated == 0 {
return Err(TaskRuntimeError::TaskNotFound);
}
Ok(())
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add a version check and one transaction to archive_task.

archive_task archives the task and every descendant with no optimistic-concurrency check. update_task requires a matching version and returns VersionConflict. Archiving is more destructive than an update, so it needs at least the same protection.

The membership check with self.get_task and the recursive UPDATE also run on two separate statements without a transaction. Another writer can reparent or archive rows between the two steps, so the archived subtree can differ from the verified subtree.

Take a version for the target task, and run the check plus the UPDATE inside one TransactionBehavior::Immediate transaction, as update_task does at Lines 440-441. The IPC route todos.archive in executor/src/local/app_ipc.rs (Lines 1013-1021) must then forward the version.

🤖 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/src/task_runtime/store.rs` around lines 504 - 528, Update
archive_task to accept the target task version, start a
TransactionBehavior::Immediate transaction before the task check, and perform
both get_task validation and the recursive archive update through that
transaction. Add the matching version predicate to the target-row update, return
VersionConflict when no row is updated due to a version mismatch, and commit the
transaction on success. Update the todos.archive IPC route to receive and
forward the version argument.

Comment on lines +53 to 66
async function extractZip(archive, destination) {
const zip = await JSZip.loadAsync(await readFile(archive))
await Promise.all(
Object.values(zip.files).map(async entry => {
const output = join(destination, entry.name)
if (entry.dir) {
await mkdir(output, { recursive: true })
return
}
await mkdir(dirname(output), { recursive: true })
await writeFile(output, await entry.async('nodebuffer'))
})
)
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject archive entries that escape the destination.

join(destination, entry.name) follows .. segments. A crafted entry name such as ../../evil.js writes outside temporaryDirectory. The previous implementation delegated to an external extractor; this in-process extractor must perform the check itself.

Resolve each output path and confirm that it stays inside the destination.

🛡️ Proposed fix
 async function extractZip(archive, destination) {
   const zip = await JSZip.loadAsync(await readFile(archive))
+  const root = resolve(destination)
   await Promise.all(
     Object.values(zip.files).map(async entry => {
-      const output = join(destination, entry.name)
+      const output = resolve(destination, entry.name)
+      if (output !== root && !output.startsWith(`${root}${sep}`)) {
+        throw new Error(`Refusing to extract entry outside the destination: ${entry.name}`)
+      }
       if (entry.dir) {
         await mkdir(output, { recursive: true })
         return
       }
       await mkdir(dirname(output), { recursive: true })
       await writeFile(output, await entry.async('nodebuffer'))
     })
   )
 }

Add sep to the node:path import:

-import { dirname, join, resolve } from 'node:path'
+import { dirname, join, resolve, sep } from 'node:path'
📝 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
async function extractZip(archive, destination) {
const zip = await JSZip.loadAsync(await readFile(archive))
await Promise.all(
Object.values(zip.files).map(async entry => {
const output = join(destination, entry.name)
if (entry.dir) {
await mkdir(output, { recursive: true })
return
}
await mkdir(dirname(output), { recursive: true })
await writeFile(output, await entry.async('nodebuffer'))
})
)
}
async function extractZip(archive, destination) {
const zip = await JSZip.loadAsync(await readFile(archive))
const root = resolve(destination)
await Promise.all(
Object.values(zip.files).map(async entry => {
const output = resolve(destination, entry.name)
if (output !== root && !output.startsWith(`${root}${sep}`)) {
throw new Error(`Refusing to extract entry outside the destination: ${entry.name}`)
}
if (entry.dir) {
await mkdir(output, { recursive: true })
return
}
await mkdir(dirname(output), { recursive: true })
await writeFile(output, await entry.async('nodebuffer'))
})
)
}
🤖 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/scripts/prepare-dws-binary.mjs` around lines 53 - 66, Update
extractZip to resolve the destination and each archive entry path, then reject
entries whose resolved output is outside the destination boundary before
creating directories or writing files. Use the node:path separator as needed to
enforce a proper descendant check while allowing paths within the destination.

Comment on lines +390 to +393
<header className="pb-7">
<h1 className="text-heading-lg font-semibold">管理项目</h1>
<p className="mt-1 text-sm text-text-muted">管理项目成员、标签和看板布局。</p>
</header>

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Board settings UI bypasses the Wework i18n pipeline. Both files render user-facing copy as hardcoded Chinese literals instead of translation keys, and the manage view dropped its existing translation usage. The shared root cause is missing i18n wiring for the new board-settings surface.

  • wework/src/features/todo/CloudProjectManageView.tsx#L390-L393: restore useTranslation('common') and replace every literal in this view, including headings, member and tag controls, confirmation prompts, and error fallbacks, with translation keys.
  • wework/src/features/todo/BoardLayoutEditor.tsx#L243-L248: use the same wrapper for the section heading, helper text, display-menu labels, status menu labels, aria-label values, and the default 新状态 name.

Add the new keys to both wework/src/i18n/locales/en/ and wework/src/i18n/locales/zh-CN/, and update BoardLayoutEditor.test.tsx so it does not assert raw copy.

As per coding guidelines: "Use the local @/hooks/useTranslation wrapper for new Wework code" and "Add new copy to the appropriate Wework namespace in both src/i18n/locales/en/ and src/i18n/locales/zh-CN/; register new namespaces in src/i18n/index.ts".

📍 Affects 2 files
  • wework/src/features/todo/CloudProjectManageView.tsx#L390-L393 (this comment)
  • wework/src/features/todo/BoardLayoutEditor.tsx#L243-L248
🤖 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/todo/CloudProjectManageView.tsx` around lines 390 - 393,
Replace hardcoded user-facing copy with the local useTranslation('common')
wrapper: in wework/src/features/todo/CloudProjectManageView.tsx:390-393 and
throughout the view, restore translation usage for headings, controls, prompts,
and error fallbacks; in wework/src/features/todo/BoardLayoutEditor.tsx:243-248
and throughout the editor, translate headings, helper text, menu labels,
aria-labels, and the default 新状态 name. Add matching keys to both
wework/src/i18n/locales/en/ and wework/src/i18n/locales/zh-CN/, register any new
namespace in wework/src/i18n/index.ts, and update BoardLayoutEditor.test.tsx to
assert translated output rather than raw copy.

Source: Coding guidelines

Comment on lines +82 to +201
const nativeBoardGroupFields: AITableField[] = [
{ id: 'status', name: '状态', type: 'status', config: null, raw: {} },
{ id: 'priority', name: '优先级', type: 'singleSelect', config: null, raw: {} },
{ id: 'assignee', name: '负责人', type: 'user', config: null, raw: {} },
{ id: 'tag', name: '标签', type: 'tag', config: null, raw: {} },
]

const nativeBoardStatusColors: Record<
CloudLoopItem['status'],
'gray' | 'blue' | 'orange' | 'purple' | 'green'
> = {
inbox: 'gray',
pending: 'blue',
in_progress: 'orange',
in_review: 'purple',
completed: 'green',
}

function aitableCellLabels(value: unknown): string[] {
if (value === null || value === undefined || value === '') return []
return (Array.isArray(value) ? value : [value])
.map(entry => {
if (typeof entry === 'object' && entry !== null) {
const object = entry as Record<string, unknown>
return String(object.name ?? object.title ?? object.text ?? '')
}
return String(entry)
})
.filter(Boolean)
}

function AITableGroupFieldPicker({
fields,
value,
onChange,
testIdPrefix = 'dingtalk-board-group',
searchPlaceholder = '搜索表格字段',
}: {
fields: AITableField[]
value: string
onChange: (fieldId: string) => void
testIdPrefix?: string
searchPlaceholder?: string
}) {
const rootRef = useRef<HTMLDivElement>(null)
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const selected = fields.find(field => field.id === value)
const visibleFields = fields
.filter(field => `${field.name} ${field.type}`.toLowerCase().includes(query.toLowerCase()))
.sort((left, right) => {
const recommended = (field: AITableField) =>
/状态|负责人|优先级|所属项目/.test(field.name) ? 0 : 1
return recommended(left) - recommended(right)
})

useEffect(() => {
if (!open) return
const close = (event: MouseEvent) => {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', close)
return () => document.removeEventListener('mousedown', close)
}, [open])

return (
<div ref={rootRef} className="relative">
<button
type="button"
data-testid={`${testIdPrefix}-by`}
onClick={() => setOpen(current => !current)}
className="flex h-8 min-w-32 items-center justify-between gap-2 rounded-lg border border-border bg-background px-3 text-xs text-text-secondary hover:bg-muted"
aria-expanded={open}
>
<span className="max-w-32 truncate">{selected?.name ?? '选择分组字段'}</span>
<ChevronDown className="h-3 w-3 shrink-0" />
</button>
{open ? (
<div className="absolute left-0 top-9 z-40 w-64 overflow-hidden rounded-xl border border-border bg-background p-1.5 shadow-lg">
<label className="flex h-8 items-center gap-2 rounded-lg bg-muted px-2.5 text-text-muted">
<Search className="h-3.5 w-3.5" />
<input
autoFocus
data-testid={`${testIdPrefix}-search`}
value={query}
onChange={event => setQuery(event.target.value)}
placeholder={searchPlaceholder}
className="min-w-0 flex-1 bg-transparent text-xs text-text-primary outline-none"
/>
</label>
<div className="mt-1 max-h-72 overflow-y-auto overscroll-contain">
{visibleFields.map(field => (
<button
key={field.id}
type="button"
data-testid={`${testIdPrefix}-option-${field.id}`}
onClick={() => {
onChange(field.id)
setOpen(false)
setQuery('')
}}
className={cn(
'flex h-9 w-full items-center rounded-lg px-2.5 text-left text-sm hover:bg-muted',
field.id === value && 'bg-muted font-medium'
)}
>
<span className="min-w-0 flex-1 truncate">{field.name}</span>
<span className="ml-2 shrink-0 text-xs text-text-muted">{field.type}</span>
{field.id === value ? <Check className="ml-2 h-3.5 w-3.5" /> : null}
</button>
))}
{visibleFields.length === 0 ? (
<p className="px-3 py-6 text-center text-xs text-text-muted">没有匹配字段</p>
) : null}
</div>
</div>
) : null}
</div>
)
}

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.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split this file; it is now 2449 lines.

The repository guideline sets a 1000-line limit, and this change grows the file further. Three extractions are self-contained and reduce the component body directly:

  • Move AITableGroupFieldPicker (lines 113-201) into its own module. CloudTodoBoardCard.tsx already shows this pattern.
  • Move nativeBoardGroupFields, nativeBoardStatusColors, and aitableCellLabels (lines 82-111) into todoShared.ts.
  • Move the boardColumns construction (lines 905-982), which is a five-level nested ternary of about 78 lines recomputed on every render, into a memoized helper module.

As per coding guidelines: "Favor cohesive modules, explicit interfaces, and standard practices; split files over 1000 lines".

🤖 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/todo/CloudTodoWorkspace.tsx` around lines 82 - 201, Split
CloudTodoWorkspace into cohesive modules to keep it under the 1000-line
guideline: move AITableGroupFieldPicker into its own module following
CloudTodoBoardCard.tsx’s pattern, move nativeBoardGroupFields,
nativeBoardStatusColors, and aitableCellLabels into todoShared.ts, and extract
the boardColumns construction into a memoized helper module while preserving its
existing behavior and interfaces.

Source: Coding guidelines

Comment on lines +1328 to 1344
} else {
setItems(current =>
current.map(candidate =>
candidate.id === item.id ? { ...candidate, source_status: sourceStatus } : candidate
)
)
try {
const itemApi = apiForProjectId(item.cloud_project_id)
if (!itemApi) throw new Error('项目空间当前不可用')
const updated = await itemApi.updateLoopItem(item.id, {
version: item.version,
status: sourceStatus as CloudLoopItem['status'],
current.map(candidate => {
if (candidate.id !== itemId) return candidate
if (nativeGroupBy === 'priority') {
return { ...candidate, priority: column.groupValue as CloudLoopItem['priority'] }
}
if (nativeGroupBy === 'assignee') {
return {
...candidate,
assignee_user_id: column.groupValue ? Number(column.groupValue) : null,
}
}
return { ...candidate, tags: column.groupValue ? [column.groupValue] : [] }
})
setItems(current =>
current.map(candidate => (candidate.id === updated.id ? updated : candidate))
)
} catch (cause) {
setItems(previousItems)
setBoardError(cause instanceof Error ? cause.message : '移动任务失败')
}
return
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Dragging a card between tag columns deletes its other tags.

For nativeGroupBy === 'tag', line 1341 and line 1356 both set tags to a single-element array built from column.groupValue. A task with ['发布', '线上问题'] dragged into the 发布 column keeps only 发布. An empty groupValue, which is the 无标签 column, clears every tag. The gesture therefore destroys data that the user did not intend to change, and there is no confirmation. Replace only the tag that the current grouping owns, and keep the remaining tags.

🐛 Proposed fix to preserve tags outside the grouping
+    const retagItem = (candidate: CloudLoopItem): string[] => {
+      const groupTags = new Set(availableTags)
+      const preserved = (candidate.tags ?? []).filter(tag => !groupTags.has(tag))
+      return column.groupValue ? [...preserved, column.groupValue] : preserved
+    }
-          return { ...candidate, tags: column.groupValue ? [column.groupValue] : [] }
+          return { ...candidate, tags: retagItem(candidate) }
-              : { tags: column.groupValue ? [column.groupValue] : [] }
+              : { tags: retagItem(item) }

Also applies to: 1349-1356

🤖 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/todo/CloudTodoWorkspace.tsx` around lines 1328 - 1344,
Update the tag-handling branches in the item move logic, including the mapping
callback and the corresponding code around the alternate path, so dragging
between tag columns replaces only the tag owned by the current grouping while
preserving all other tags. Treat an empty column.groupValue as removing only the
grouping tag rather than clearing candidate.tags, and leave priority and
assignee behavior unchanged.

Comment on lines 2223 to 2231
<DragOverlay dropAnimation={null}>
{activeDragItemId ? (
<div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
<TodoCardContent
<CloudTodoCardContent
item={items.find(item => item.id === activeDragItemId)!}
display={boardCardDisplay}
/>
</div>
) : null}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The drag overlay dereferences a possibly missing item.

Line 2227 uses a non-null assertion on items.find(...). The board refresh interval at lines 1272-1274 replaces items every 15 seconds, and confirmArchiveItem removes items too. If the dragged item disappears from items while a drag is active, find returns undefined and CloudTodoCardContent throws on item.title, which unmounts the workspace. Resolve the item first and render the overlay only when it exists.

🐛 Proposed fix to remove the non-null assertion
         <DragOverlay dropAnimation={null}>
-          {activeDragItemId ? (
-            <div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
-              <CloudTodoCardContent
-                item={items.find(item => item.id === activeDragItemId)!}
-                display={boardCardDisplay}
-              />
-            </div>
-          ) : null}
+          {(() => {
+            const draggedItem = activeDragItemId
+              ? (items.find(item => item.id === activeDragItemId) ?? null)
+              : null
+            if (!draggedItem) return null
+            return (
+              <div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
+                <CloudTodoCardContent item={draggedItem} display={boardCardDisplay} />
+              </div>
+            )
+          })()}
         </DragOverlay>
📝 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
<DragOverlay dropAnimation={null}>
{activeDragItemId ? (
<div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
<TodoCardContent
<CloudTodoCardContent
item={items.find(item => item.id === activeDragItemId)!}
display={boardCardDisplay}
/>
</div>
) : null}
<DragOverlay dropAnimation={null}>
{(() => {
const draggedItem = activeDragItemId
? (items.find(item => item.id === activeDragItemId) ?? null)
: null
if (!draggedItem) return null
return (
<div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
<CloudTodoCardContent item={draggedItem} display={boardCardDisplay} />
</div>
)
})()}
</DragOverlay>
🤖 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/todo/CloudTodoWorkspace.tsx` around lines 2223 - 2231,
Update the DragOverlay rendering in CloudTodoWorkspace to resolve the active
item from items and render CloudTodoCardContent only when that lookup returns an
item; remove the non-null assertion from items.find(...). Preserve the existing
overlay styling and null rendering when there is no active drag item or the item
has disappeared.

Comment on lines +733 to 739
{status === '' ? <option value="">未设置</option> : null}
{statusOptions.map(option => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
</select>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the CloudLoopItem status type definition.
rg -nP -C 6 "status:\s*" wework/src/api/deliveries.ts | head -60
ast-grep run --pattern 'export interface CloudLoopItem { $$$ }' --lang typescript wework/src/api/deliveries.ts

Repository: wecode-ai/Wegent

Length of output: 3410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate TodoEditor and inspect relevant sections"
fd -a 'TodoEditor\.tsx$' . | sed 's#^\./##'
echo

echo "Outline TodoEditor"
ast-grep outline wework/src/features/todo/TodoEditor.tsx --match 'TodoEditor' --view expanded 2>/dev/null || true
echo

echo "Status-related code around select and save"
rg -n "statusOptions|status === ''|onChange|statusOptions|board_config|statusOptions|save|reorder|update" wework/src/features/todo/TodoEditor.tsx | sed -n '1,220p'
echo

echo "File ranges 700-770 and status handling area"
nl -ba wework/src/features/todo/TodoEditor.tsx | sed -n '680,770p'
echo "Status and statusOptions definitions/assignments"
rg -n -C 4 "set\s*status|statusOptions|use.*status|board_config|getStatus" wework/src/features/todo/TodoEditor.tsx

Repository: wecode-ai/Wegent

Length of output: 2525


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n wework/src/features/todo/TodoEditor.tsx | sed -n '200,480p'
echo
cat -n wework/src/features/todo/TodoEditor.tsx | sed -n '700,745p'
echo
cat -n wework/src/features/todo/TodoEditor.tsx | sed -n '1080,1125p'

Repository: wecode-ai/Wegent

Length of output: 17343


Render the current status when it is not in statusOptions.

In edit mode, status is initialized from item.status, but the <select> only creates the placeholder option when status === ''. If an old item has a status not present in project.board_config.statuses, the element selects the first option while status stays unchanged; the subsequent save then sends that stale status. Add the missing current status as an option when needed.

🤖 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/todo/TodoEditor.tsx` around lines 733 - 739, Update the
status select rendering in TodoEditor so that when the current status is
non-empty and absent from statusOptions, it adds an option for that status,
while preserving the existing placeholder and mapped options. Use the current
status value and a suitable label, and ensure normal statuses already present in
statusOptions are not duplicated.

@qdaxb
qdaxb enabled auto-merge July 31, 2026 03:54
auto-merge was automatically disabled July 31, 2026 06:24

Head branch was pushed to by a user without write access

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (2)
wework/src-tauri/src/system_sleep.rs (2)

257-260: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle SetThreadExecutionState failures when clearing inhibition.

SetThreadExecutionState returns zero on failure, but the Windows cleanup path discards the result. A failed ES_CONTINUOUS reset can leave sleep inhibition active without logging. Capture the return value and log a warning when it is zero.

🤖 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-tauri/src/system_sleep.rs` around lines 257 - 260, Update the
cleanup path around SetThreadExecutionState when clearing inhibition to capture
its return value and log a warning if it is zero, while preserving the existing
ES_CONTINUOUS reset behavior.

166-169: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep Windows sleep inhibition on the same OS thread.

SetThreadExecutionState is per-thread under Windows, and ES_CONTINUOUS remains active until the acquiring thread calls it again with only ES_CONTINUOUS. SystemSleepState is shared across Tauri task threads, so SleepInhibitor::acquire() and Drop can run on different threads; the mismatch branch only logs instead of releasing the power request. Use a dedicated stable OS thread or thread-affine ownership, and add real-Tauri Windows coverage for task start, task settlement, and inhibitor release.

🤖 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-tauri/src/system_sleep.rs` around lines 166 - 169, Update the
Windows-specific SleepInhibitor acquire/drop flow and shared SystemSleepState
ownership so SetThreadExecutionState is always acquired and released on the same
stable OS thread; do not merely log when thread IDs differ. Use dedicated
thread-affine ownership, and add real-Tauri Windows coverage covering task
start, task settlement, and inhibitor release.

Source: Coding guidelines

🧹 Nitpick comments (1)
wework/src-tauri/src/system_sleep.rs (1)

6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused Windows process-spawn declarations from Windows sleep inhibition.

system_sleep.rs only uses SetThreadExecutionState on Windows, so CommandExt and CREATE_NO_WINDOW are unused here. Remove these declarations with the obsolete PowerShell path to keep the Windows implementation simpler.

Proposed cleanup
-#[cfg(target_os = "windows")]
-use std::os::windows::process::CommandExt;
-
-#[cfg(target_os = "windows")]
-const CREATE_NO_WINDOW: u32 = 0x0800_0000;
🤖 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-tauri/src/system_sleep.rs` around lines 6 - 11, Remove the
Windows-only std::os::windows::process::CommandExt import and CREATE_NO_WINDOW
constant from system_sleep.rs, along with the obsolete PowerShell process-spawn
path that depends on them. Preserve the existing SetThreadExecutionState-based
sleep inhibition implementation and unrelated task ID handling.

Source: Coding guidelines

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

Outside diff comments:
In `@wework/src-tauri/src/system_sleep.rs`:
- Around line 257-260: Update the cleanup path around SetThreadExecutionState
when clearing inhibition to capture its return value and log a warning if it is
zero, while preserving the existing ES_CONTINUOUS reset behavior.
- Around line 166-169: Update the Windows-specific SleepInhibitor acquire/drop
flow and shared SystemSleepState ownership so SetThreadExecutionState is always
acquired and released on the same stable OS thread; do not merely log when
thread IDs differ. Use dedicated thread-affine ownership, and add real-Tauri
Windows coverage covering task start, task settlement, and inhibitor release.

---

Nitpick comments:
In `@wework/src-tauri/src/system_sleep.rs`:
- Around line 6-11: Remove the Windows-only
std::os::windows::process::CommandExt import and CREATE_NO_WINDOW constant from
system_sleep.rs, along with the obsolete PowerShell process-spawn path that
depends on them. Preserve the existing SetThreadExecutionState-based sleep
inhibition implementation and unrelated task ID handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d45c1d5-3f8e-4094-96fd-5f97ec2cda98

📥 Commits

Reviewing files that changed from the base of the PR and between 68c92be and c2fd647.

📒 Files selected for processing (3)
  • wework/package.json
  • wework/src-tauri/src/system_sleep.rs
  • wework/src/features/todo/AITableView.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • wework/src/features/todo/AITableView.test.tsx
  • wework/package.json

@icycrystal4
icycrystal4 added this pull request to the merge queue Jul 31, 2026
Merged via the queue into wecode-ai:main with commit 308298d Jul 31, 2026
38 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.

2 participants