Skip to content

refactor(wework): redesign cloud kanban workspace - #2215

Merged
qdaxb merged 3 commits into
wecode-ai:mainfrom
Micro66:human/giraffe-20260724-105636
Jul 25, 2026
Merged

refactor(wework): redesign cloud kanban workspace#2215
qdaxb merged 3 commits into
wecode-ai:mainfrom
Micro66:human/giraffe-20260724-105636

Conversation

@Micro66

@Micro66 Micro66 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added cloud project-space mentions with project, task, file, and delivery navigation.
    • Added cloud TODO creation/editing with tagging, collaborators, attachments, and soft-delete with recycle-bin restore.
    • Added status-scoped loop-item reordering (including lane drag-and-drop) and project tag management.
    • Added “My Work” grouped/list/calendar/timeline views, plus cloud project management UI.
  • Bug Fixes
    • Deleted TODOs are hidden from active views and restoration validates deleted state and hierarchy rules.
    • Improved mention filtering and delivered richer, structured error messages for tool failures.

Micro66 added 2 commits July 25, 2026 21:05
- Split CloudTodoWorkspace into My Work view (calendar + list) and
  project manage view, backed by a shared cloudMyWorkModel
- Add TodoEditor and TagEditor, replacing CloudProjectSettingsDialog
- Extend composer mentions with cloud-space candidates
- Support loop item soft delete, tagging schema, and delivery tool
  updates in the backend
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a67e0b73-c6a3-4fb8-a0cf-fec1eed077f0

📥 Commits

Reviewing files that changed from the base of the PR and between a53df04 and 724c1f6.

📒 Files selected for processing (3)
  • executor/src/agents/codex.rs
  • wework/package.json
  • wework/src/components/layout/DesktopWorkbenchMain.tsx

📝 Walkthrough

Walkthrough

This change adds cloud project-space mentions, cloud TODO CRUD and collaboration tools, tag normalization, soft deletion, lane reordering, redesigned workspace views, reusable TODO editing, interactive demos, MCP protocol metadata, and persistent tool-search configuration across backend, frontend, demos, and executor code.

Changes

Delivery backend and MCP

Layer / File(s) Summary
Tags, soft deletion, and reorder contracts
backend/alembic/..., backend/app/models/delivery.py, backend/app/schemas/*, backend/app/services/loop_items/service.py, backend/app/api/endpoints/deliveries.py
Loop items gain metadata-backed tags, soft-delete lifecycle operations, deleted-item filtering, lane reorder handling, and a reorder API endpoint.
Delivery MCP resources and TODO tools
backend/app/mcp_server/server.py, backend/app/mcp_server/tools/delivery.py, backend/tests/mcp_server/*
Delivery MCP resources and tools support project references, cloud project creation, TODO lifecycle operations, recycle-bin access, collaborators, attachments, authorization, and structured serialization.
Runtime MCP naming and errors
backend/app/mcp_server/tool_registry.py, backend/app/services/runtime_work_service.py, wework/src/api/local/*
The delivery server identifier becomes wegent_delivery, cloud references activate project-space context, and tool failures return structured error objects.

Cloud project-space mentions

Layer / File(s) Summary
Mention matching and scope contracts
wework/src/components/chat/composer/composerAutocomplete.ts, composerMentionCandidates.ts, useComposerMentionCandidates.ts, *.test.ts
Mention matching supports project metadata, status labels, reusable candidate filtering, whitespace-aware triggers, and project-space scope parsing.
Composer project-space navigation
wework/src/components/chat/ChatInput.tsx, wework/src/components/chat/composer/*
Composer props and menu rows support direct cloud-space references, project browsing, scoped filtering, TODO status badges, and project-selection callbacks.
Mention integration validation
wework/src/components/layout/DesktopWorkbenchMain.tsx, wework/src/components/chat/composer/ComposerTextarea.test.tsx, wework/e2e/desktop/scenarios/*
The workbench loads project candidates, binds selected projects, displays notices, and tests the complete mention interaction flow.

Cloud TODO workspace

Layer / File(s) Summary
Board ordering and workspace navigation
wework/src/features/todo/CloudTodoWorkspace.tsx, todoShared.ts, wework/src/api/deliveries.ts
Board drag-and-drop uses shared lane ordering and persisted reorder requests; workspace navigation adds My Work, files, management, tag filtering, member state, and cross-project detail synchronization.
Reusable TODO editor and tag management
wework/src/features/todo/TodoEditor.tsx, TagEditor.tsx, CloudProjectManageView.tsx, CloudTodoModal.tsx, TodoBindingPicker.tsx
TODO creation and editing handle drafts, metadata, tags, collaborators, attachments, deliveries, keyboard actions, and versioned updates; project management handles members and tags.
My Work views
wework/src/features/todo/CloudMyWorkView.tsx, CloudMyWorkCalendar.tsx, cloudMyWorkModel.ts, cloud-my-work-calendar.css, tests
My Work supports grouped, list, calendar, and timeline layouts with sorting, date grouping, localized labels, event selection, and test coverage.

Interactive demos and executor support

Layer / File(s) Summary
Cloud-space and Kanban demos
demo/cloud-space-mention/*, demo/kanban-redesign/*
Static demos add cloud mention scenes and a Kanban workspace with board, files, project, editor, overlay, calendar, and timeline views.
Desktop E2E scenario
wework/e2e/desktop/scenarios/cloud-space-mention.scenario.mjs
The desktop scenario stubs cloud APIs and verifies project-space mention navigation, filtering, binding, chip insertion, screenshots, and diagnostics.
Persistent tool-search startup
executor/src/agents/codex.rs, executor/src/agents/codex/tests.rs
Persistent Codex app-server startup enables features.tool_search=true and verifies the override.
Editor and calendar test support
wework/package.json, wework/src/test/setup.ts
Editor and calendar packages are added, and browser API plus local-storage test setup is extended.

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 19.33% 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 is concise and accurately reflects the PR’s main theme: a cloud kanban workspace redesign in the wework app.
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: 9

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 (4)
wework/src/features/todo/CloudTodoWorkspace.tsx (3)

956-967: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Files tab is missing a data-testid.

Its siblings expose cloud-project-board-view and cloud-project-manage-view, but the 文件 tab has none, so E2E/unit coverage can't target it.

As per coding guidelines: "All new interactive elements must have descriptive data-testid values."

🔧 Proposed fix
                 <button
                   type="button"
+                  data-testid="cloud-project-files-view"
                   onClick={() => setProjectView('files')}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx` around lines 956 - 967, Add
a descriptive data-testid to the 文件 button in the project view tab group,
matching the existing cloud-project-* naming convention and distinguishing the
files view from the board and manage tabs.

Source: Coding guidelines


598-617: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Rollback undoes a status change that already committed on the server.

If updateLoopItem succeeds but reorderLoopItems then fails, the catch restores previousItems, so the card jumps back to its old lane even though the backend has already moved it. The UI then disagrees with the server until the 15s poll. Rolling back only the ordering (keeping the persisted status) — or refetching the lane on failure — keeps the two in sync.

🤖 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 598 - 617, The
error path in the move flow around updateLoopItem and reorderLoopItems
incorrectly restores the entire previousItems state after a status update may
have succeeded. Adjust the catch handling to preserve the committed status
change while rolling back only ordering, or refetch the affected lane from the
server, so the UI remains consistent with persisted data.

496-521: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Mount now issues 2×N requests to populate the project list.

Every project triggers both listLoopItems (only .length is used) and listCloudProjectMembers, and Promise.all fires them all at once — for a user in many project spaces this is a burst of requests plus full item payloads discarded except for a count. A summary field on listCloudProjects (item count + top members) would collapse this to one request; failing that, load members lazily for the project table only.

Also note the .then(...).finally(...) chain has no .catch: a failed listCloudProjects/listLoopItems clears loading but leaves the user on an empty "创建第一个项目空间" state with no error, which is indistinguishable from having no projects. Consider surfacing the failure.

🤖 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 496 - 521,
Update the project-loading useEffect to avoid per-project 2×N requests and
discarded loop-item payloads: use summary data from listCloudProjects for counts
and members when available, or defer listCloudProjectMembers loading to the
project table. Add a catch path for failures from listCloudProjects or the
per-project loading calls that records and surfaces an error instead of showing
the empty-project state, while preserving loading cleanup.
wework/src/features/todo/TaskDescriptionEditor.tsx (1)

150-161: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Bullet and ordered list buttons now show the identical label “列表”.

Only the icon and title/aria-label distinguish them; visible text should differ (e.g. 列表 / 编号) so the two adjacent buttons are not ambiguous.

✏️ Proposed fix
           <ListOrdered className="h-3.5 w-3.5" />
-          列表
+          编号
🤖 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/TaskDescriptionEditor.tsx` around lines 150 - 161,
Update the ordered-list ToolbarButton in TaskDescriptionEditor so its visible
label differs from the bullet-list button, using “编号” while preserving the
existing bullet-list “列表” label and ordered-list behavior.
🟡 Minor comments (15)
wework/src/i18n/locales/en/common.json-1477-1477 (1)

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

Fix the cloud-space description in both locales. The newly added copy is unclear and ungrammatical, so users may not understand what the mention provides.

  • wework/src/i18n/locales/en/common.json#L1477-L1477: replace it with clear English such as Use cloud-space capabilities to perform any action you have permission to take in natural language.
  • wework/src/i18n/locales/zh-CN/common.json#L1476-L1476: replace it with natural Chinese such as 使用云空间能力,用自然语言执行你有权限的操作。
🤖 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` at line 1477, Update the
mention_cloud_project_space_description translation in
wework/src/i18n/locales/en/common.json at lines 1477-1477 with clear English
describing cloud-space capabilities and permitted actions in natural language,
and update the same key in wework/src/i18n/locales/zh-CN/common.json at lines
1476-1476 with natural Chinese conveying the equivalent meaning.
wework/src/components/chat/composer/composerMentionCandidates.test.ts-7-18 (1)

7-18: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

CloudProject test fixtures omit the non-optional tags field. CloudProject (wework/src/api/deliveries.ts lines 72-84) declares tags: string[], so both annotated object literals should fail type checking.

  • wework/src/components/chat/composer/composerMentionCandidates.test.ts#L7-L18: add tags: [] to CLOUD_PROJECT.
  • wework/src/components/chat/composer/ComposerTextarea.test.tsx#L28-L39: add tags: [] to WEBSITE_PROJECT (MOBILE_PROJECT inherits it via spread).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/chat/composer/composerMentionCandidates.test.ts` around
lines 7 - 18, Add the required empty tags array to the CLOUD_PROJECT fixture in
wework/src/components/chat/composer/composerMentionCandidates.test.ts lines 7-18
and to the WEBSITE_PROJECT fixture in
wework/src/components/chat/composer/ComposerTextarea.test.tsx lines 28-39;
MOBILE_PROJECT inherits the field through its spread and needs no direct change.
wework/src/api/local/localServices.ts-1144-1159 (1)

1144-1159: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check for an existing trusted capability entry.

An untrusted projectSpaceCapability entry is excluded at Line 1143 but still makes context?.projectSpaceCapability truthy, suppressing the generated capability instructions.

Proposed fix
-  if (message.includes('cloud://projects') && !context?.projectSpaceCapability) {
+  if (
+    message.includes('cloud://projects') &&
+    !entries.some(([name]) => name === 'projectSpaceCapability')
+  ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/api/local/localServices.ts` around lines 1144 - 1159, Update the
project-space capability check around the `projectSpaceCapability` entry
generation to determine whether an existing entry is trusted, rather than
relying on the truthiness of `context?.projectSpaceCapability`. Ensure untrusted
entries excluded near this block do not suppress the generated capability
instructions, while preserving the existing behavior for trusted entries.
backend/app/schemas/delivery.py-50-55 (1)

50-55: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject duplicate IDs in reorder requests.

The service retains duplicate item_ids, so one TODO can be returned twice and assigned multiple positions. Validate uniqueness before persistence.

Proposed fix
 class LoopItemReorder(BaseModel):
@@
     item_ids: list[str] = Field(min_length=1, max_length=1000)
+
+    `@field_validator`("item_ids")
+    `@classmethod`
+    def require_unique_item_ids(cls, value: list[str]) -> list[str]:
+        if len(value) != len(set(value)):
+            raise ValueError("item_ids must not contain duplicates")
+        return value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/schemas/delivery.py` around lines 50 - 55, Update the
LoopItemReorder schema to validate that item_ids contains only unique IDs before
persistence, while preserving its existing non-empty and maximum-length
constraints.
backend/app/mcp_server/tool_registry.py-133-144 (1)

133-144: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Error payload shape is now inconsistent within the same wrapper.

The auth failure at Line 102 still returns {"error": "<string>"} while this path returns {"error": {code, message, ...}}. Any consumer that reads error must now handle both a string and an object. Emit the structured shape for the auth failure too (e.g. code: "MCP_AUTH_REQUIRED", retryable: False).

🔧 Proposed fix
         ctx = get_mcp_context()
         if not ctx or not ctx.token_info:
-            return json.dumps({"error": "Authentication required"})
+            return json.dumps(
+                {
+                    "error": {
+                        "code": "MCP_AUTH_REQUIRED",
+                        "message": "Authentication required",
+                        "server": server_name,
+                        "tool": tool_name,
+                        "retryable": False,
+                    }
+                },
+                ensure_ascii=False,
+            )
🤖 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/mcp_server/tool_registry.py` around lines 133 - 144, Update the
authentication failure path in the tool wrapper to return the same structured
error object shape used by the MCP_TOOL_EXECUTION_FAILED response. Include the
MCP_AUTH_REQUIRED code, the existing authentication message, and retryable set
to false while preserving the surrounding response serialization.
wework/src/features/todo/CloudMyWorkView.tsx-74-83 (1)

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

Date-only due_at is round-tripped through local time in both My Work views. TodoEditor submits the due date as YYYY-MM-DD, so due_at arrives as a UTC-midnight instant; both views convert it via new Date(...) in local time, which shifts the day backwards for negative UTC offsets. Normalize the date portion once in cloudMyWorkModel.ts and share it.

  • wework/src/features/todo/CloudMyWorkView.tsx#L74-L83: parse due_at.slice(0, 10) into a local Date in dueDayOf instead of new Date(item.due_at), so list/timeline day grouping and 今天/明天/昨天 labels match the stored date.
  • wework/src/features/todo/CloudMyWorkCalendar.tsx#L35-L47: pass the date-only string (item.due_at.slice(0, 10)) as the all-day event start so FullCalendar places the event on the stored calendar day rather than a timezone-shifted one.
🤖 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/CloudMyWorkView.tsx` around lines 74 - 83, Normalize
date-only due_at values in cloudMyWorkModel.ts and reuse that handling across
both views. In wework/src/features/todo/CloudMyWorkView.tsx lines 74-83, update
dueDayOf to parse item.due_at.slice(0, 10) as a local date before startOfDay; in
wework/src/features/todo/CloudMyWorkCalendar.tsx lines 35-47, pass the sliced
date-only string as the all-day event start.
wework/src/features/todo/CloudTodoWorkspace.tsx-986-1006 (1)

986-1006: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Transparent overlay select has no visible focus indicator.

The real control is opacity-0 and absolutely fills the styled span, so keyboard focus produces no visible change on the wrapper. Add a focus-within: ring/border on the span so tab focus is perceivable.

♿ Suggested change
-                    <span className="relative z-10 ml-2 inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full border border-border bg-background px-2.5 text-xs transition hover:bg-muted">
+                    <span className="relative z-10 ml-2 inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full border border-border bg-background px-2.5 text-xs transition hover:bg-muted focus-within:border-focus focus-within:ring-2 focus-within:ring-focus/30">
🤖 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 986 - 1006,
Update the wrapper span around the cloud todo tag-filter select to include a
visible focus-within ring or border style. Keep the existing transparent select
and interaction unchanged, ensuring keyboard focus on the opacity-0 control
visibly changes the wrapper.
wework/src/features/todo/CloudProjectManageView.tsx-388-403 (1)

388-403: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rename confirm/cancel buttons lack data-testid.

Every other control in this view has one; these two only have aria-label, so the rename flow can't be driven from E2E/unit tests by testid.

As per coding guidelines: "All new interactive elements must have descriptive data-testid values."

🔧 Proposed fix
                     <button
                       type="button"
+                      data-testid={`cloud-project-tag-rename-confirm-${tag}`}
                       aria-label="确认重命名"
                       onClick={() => void renameTag(tag)}
@@
                     <button
                       type="button"
+                      data-testid={`cloud-project-tag-rename-cancel-${tag}`}
                       aria-label="取消重命名"
                       onClick={() => setRenamingTag(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/CloudProjectManageView.tsx` around lines 388 - 403,
添加重命名确认和取消按钮的描述性 data-testid 属性,分别应用于包含 renameTag(tag) 和 setRenamingTag(null)
的按钮。保持现有 aria-label、点击行为及样式不变,并遵循该视图中其他控件的 testid 命名约定。

Source: Coding guidelines

wework/src/features/todo/CloudTodoWorkspace.tsx-712-718 (1)

712-718: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the ⌘K binding for the todo search toggle.

searchOpen is only toggled by the button click in wework/src/features/todo/CloudTodoWorkspace.tsx; there is no key listener for this component’s search dialog, so the ⌘K badge advertises a shortcut that does nothing from the todo view. Add the handler here or reuse the existing workspace search shortcut.

🤖 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 712 - 718, Wire
a ⌘K keyboard handler into the CloudTodoWorkspace search flow so it toggles the
existing searchOpen state and opens or closes the todo search dialog, matching
the button’s behavior. Reuse an existing workspace shortcut handler if
available, and ensure the listener is scoped to this component and cleaned up
appropriately.
wework/src/features/todo/CloudTodoWorkspace.tsx-121-129 (1)

121-129: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Board cards render raw priority enum values.

For anything other than none, the badge prints the API value (low/medium/high/urgent) in an otherwise Chinese UI. It also labels none as 普通, while CloudMyWorkView.tsx (Line 60-66) maps medium → 普通 and none → 无, so the same item reads differently in the two views. Extract a shared priority label map next to priorityBadgeClasses in todoShared.ts and use it in both places.

🌐 Sketch
-        {item.priority === 'none' ? '普通' : item.priority}
+        {priorityLabels[item.priority]}
🤖 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 121 - 129,
Extract a shared priority label map beside priorityBadgeClasses in
todoShared.ts, mapping each API priority to the Chinese labels used by
CloudMyWorkView (including medium → 普通 and none → 无). Update the priority badge
in CloudTodoWorkspace and the corresponding rendering in CloudMyWorkView to use
this shared map instead of raw enum values or local mappings.
wework/src/features/todo/TagEditor.tsx-104-128 (1)

104-128: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Tag input has no accessible name once a tag exists.

placeholder is dropped when tags.length > 0, leaving the input without any label. Add a static aria-label.

♿ Proposed fix
         <input
           ref={inputRef}
           data-testid={`${testIdPrefix}-input`}
+          aria-label={placeholder}
           value={draft}
🤖 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/TagEditor.tsx` around lines 104 - 128, Add a static
aria-label to the input in TagEditor’s tag input JSX so it remains accessible
when tags already exist and the placeholder is hidden; keep the existing
placeholder behavior unchanged.
wework/src/features/todo/cloud-my-work-calendar.css-11-11 (1)

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

Stylelint failure: declaration-empty-line-before.

🎨 Proposed fix
   --fc-list-event-hover-bg-color: rgb(var(--color-muted));
+
   font-size: var(--text-xs);
🤖 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/cloud-my-work-calendar.css` at line 11, Fix the
Stylelint declaration-empty-line-before violation at the font-size declaration
by adding the required empty line before it, while preserving the existing
declaration and surrounding styles.

Source: Linters/SAST tools

wework/src/features/todo/todoShared.ts-43-60 (1)

43-60: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Dropping a card onto itself shifts it to the end of its lane.

When beforeItemId === itemId, the same-status early return doesn't trigger (laneOrder[itemIndex + 1]?.id is the next card, not the item itself), and currentLaneIds excludes itemId, so beforeIndex is -1 and the card gets appended at the lane end — plus a spurious reorderLoopItems request from moveItem (CloudTodoWorkspace.tsx Lines 587-618).

🐛 Proposed guard
   const item = items.find(candidate => candidate.id === itemId)
   if (!item) return null
+  if (beforeItemId === itemId) return 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/todoShared.ts` around lines 43 - 60, Update the
reorder guard in the item-move logic around the same-status check to return null
when beforeItemId equals itemId, preventing self-drops from reaching laneIds
insertion or triggering reorderLoopItems. Preserve the existing no-op handling
for plain lane drops and cards already immediately before their target.
wework/src/features/todo/TodoEditor.tsx-665-690 (1)

665-690: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Priority chip renders raw enum values.

Line 674 shows priority verbatim for anything but none, so the chip reads low/medium/high/urgent while the select below uses 低/普通/高/紧急. It also labels none as 普通, which the select maps to medium. Derive the label from a single mapping shared with the options.

🐛 Proposed fix
+const priorityLabels: Record<CloudLoopItem['priority'], string> = {
+  none: '无',
+  low: '低',
+  medium: '普通',
+  high: '高',
+  urgent: '紧急',
+}
-                {priority === 'none' ? '普通' : priority}
+                {priorityLabels[priority]}
-                <option value="none">无</option>
-                <option value="low">低</option>
-                <option value="medium">普通</option>
-                <option value="high">高</option>
-                <option value="urgent">紧急</option>
+                {todoDraftPriorities.map(value => (
+                  <option key={value} value={value}>
+                    {priorityLabels[value]}
+                  </option>
+                ))}
🤖 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 665 - 690, Update the
priority display in the TodoEditor priority chip to use a single mapping from
each priority value to its Chinese label, matching the select options. Reuse
that mapping for the option labels where practical, and ensure “none” displays
as 无 rather than 普通 while preserving the existing priority values and selection
behavior.
wework/src/features/todo/TodoEditor.tsx-401-410 (1)

401-410: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Staged attachment upload failures are swallowed silently.

Promise.allSettled discards rejections and the draft store is cleared right after, so files the user staged can be lost with no indication. Surface the failed names instead of dropping them.

🛡️ Proposed fix
       const results = await Promise.allSettled(
         pendingFiles.map(file => api.addLoopItemAttachment(created.id, file))
       )
+      const failed = pendingFiles.filter((_, index) => results[index]?.status === 'rejected')
+      if (failed.length > 0) {
+        setAttachmentError(`以下附件上传失败:${failed.map(file => file.name).join('、')}`)
+      }
🤖 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 401 - 410, Update the
staged attachment upload flow after creation in TodoEditor so rejected
addLoopItemAttachment results from Promise.allSettled are inspected, failed file
names are surfaced to the user, and failed files are not removed from
draftAttachmentStore or otherwise silently discarded; preserve cleanup and
onCreated behavior for fully successful uploads.
🧹 Nitpick comments (22)
wework/package.json (1)

78-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep TipTap packages on one lockstep version policy.

@tiptap/core and @tiptap/pm are pinned to 2.27.2, but the extensions use ^2.27.2; a future install can therefore mix different 2.x releases. Pin the extensions to 2.27.2 as well, or use an equivalent shared version mechanism, and verify the lockfile’s peer-compatible resolution.

🤖 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/package.json` around lines 78 - 83, Update the TipTap extension
dependencies in package.json—@tiptap/extension-link,
`@tiptap/extension-placeholder`, `@tiptap/extension-task-item`, and
`@tiptap/extension-task-list`—to use the same pinned 2.27.2 version as
`@tiptap/core` and `@tiptap/pm`, then verify the lockfile resolves peer-compatible
versions.
wework/src/test/setup.ts (1)

41-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Do not make hit-testing always report “no target” in interaction tests.

Returning [] means BlockNote side-menu or drag/drop code can never observe a hit element, so tests may miss regressions in target selection. Use a DOM-backed polyfill or scope this stub to mount-only tests, then add focused coverage for the hit-test-dependent path.

As per coding guidelines, do not add compatibility shims or fallback paths that hide primary-path defects.

🤖 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/test/setup.ts` around lines 41 - 44, Replace the unconditional
empty-result stub in the document.elementsFromPoint setup with a DOM-backed
hit-testing implementation, or remove it and scope any necessary mock only to
mount-only tests. Preserve real target discovery for BlockNote side-menu and
drag/drop interaction tests, and add focused coverage for the hit-test-dependent
path without introducing a fallback that masks primary-path defects.

Source: Coding guidelines

demo/kanban-redesign/assets/style.css (1)

1041-1048: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stylelint: missing blank line before the font-size declaration.

🎨 Proposed fix
   --fc-list-event-hover-bg-color: var(--bg-hover);
+
   font-size: 12.5px;
 }
🤖 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 `@demo/kanban-redesign/assets/style.css` around lines 1041 - 1048, In the
.calendar-card .fc rule, add a blank line between the custom property
declarations and the font-size declaration to satisfy Stylelint formatting
requirements.

Source: Linters/SAST tools

demo/kanban-redesign/new-task.html (1)

7-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse assets/style.css instead of re-declaring the design tokens here.

This page duplicates the :root token set and several component rules (.icon-btn, .badge, .btn-primary) that already exist in demo/kanban-redesign/assets/style.css, so the two will drift as the demo evolves. Linking the shared sheet and keeping only the page-specific editor styles inline would keep one source of truth.

🤖 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 `@demo/kanban-redesign/new-task.html` around lines 7 - 35, Replace the
duplicated design-token and shared component CSS in the page’s style block with
a link to the existing assets/style.css stylesheet. Retain only styles specific
to the new-task editor inline, including any page-specific layout rules, and
remove duplicate :root, .icon-btn, .badge, and .btn-primary definitions.
wework/e2e/desktop/scenarios/cloud-space-mention.scenario.mjs (1)

99-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead scaffolding: the create-project stub and diagnostics() are unreachable in this scenario.

verify() asserts the create action is gone (Lines 148 and 264) and never drives a project-creation flow, so POST /api/v1/cloud-projects, readJson(), createdProjectPayload, and diagnostics() can never execute — diagnostics() will always report null. Dropping them (or adding a scene that actually creates a project) keeps the scenario honest about what it covers.

As per coding guidelines, "Keep the system simpler after every change: reuse existing abstractions, remove obsolete paths" and "delete dead code".

Also applies to: 275-277

🤖 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/e2e/desktop/scenarios/cloud-space-mention.scenario.mjs` around lines
99 - 112, Remove the unreachable project-creation scaffolding from the scenario:
delete the POST /api/v1/cloud-projects handler, its
createdProjectPayload/readJson usage, and the related diagnostics() function and
call. Preserve the existing verify() assertions and scenario behavior, since
this scenario does not exercise project creation.

Source: Coding guidelines

demo/kanban-redesign/my-work.html (1)

8-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the demo’s FullCalendar CDN pin with the app dependency version.

wework/package.json declares @fullcalendar/core, @fullcalendar/daygrid, and @fullcalendar/react at 6.1.21, but the demo still loads the older fullcalendar@6.1.15 build, which may validate calendar behavior that differs from the app.

♻️ Proposed change
-<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.21/index.global.min.js"></script>
🤖 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 `@demo/kanban-redesign/my-work.html` around lines 8 - 9, The FullCalendar CDN
script in my-work.html is pinned to 6.1.15 while the app dependencies use
6.1.21. Update the script URL to load the 6.1.21 build, preserving the existing
global bundle and calendar initialization.
wework/src/components/chat/composer/ComposerMentionMenu.tsx (1)

108-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider replacing the nested ternary chains with per-row-kind lookup tables.

Icon, title, and description now each nest 4-6 levels of ternaries over row.kind, and adding another row kind means touching all three. A Record<MentionMenuRow['kind'], { Icon; title; description? }> (with the candidate/pathItem cases handled first) would keep this readable and exhaustively typed.

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

In `@wework/src/components/chat/composer/ComposerMentionMenu.tsx` around lines 108
- 154, Refactor the Icon, title, and description selection in the
ComposerMentionMenu row-mapping logic to use an exhaustively typed lookup keyed
by MentionMenuRow['kind'] instead of nested row.kind ternaries. Preserve
candidate and pathItem precedence, and keep their existing fallbacks while
consolidating per-row-kind Icon, title, and optional description values so
future kinds require one mapping entry.
backend/app/schemas/delivery.py (1)

45-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a named, typed tag validator.

Replace the inline lambda with a @classmethod validator, matching CloudProjectUpdate.normalize_tag_list.

Proposed refactor
-    _normalize = field_validator("tags", mode="before")(
-        lambda value: None if value is None else _normalize_tags(value)
-    )
+    `@field_validator`("tags", mode="before")
+    `@classmethod`
+    def normalize_tag_list(cls, value: object) -> object:
+        return None if value is None else _normalize_tags(value)

As per coding guidelines, **/*.py requires “clear names” and “Python type hints.”

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

In `@backend/app/schemas/delivery.py` around lines 45 - 47, Replace the inline
lambda assigned to _normalize with a named, typed `@classmethod` field validator
for tags, matching CloudProjectUpdate.normalize_tag_list. Preserve the existing
behavior of returning None for None and applying _normalize_tags otherwise, and
include explicit Python type hints.

Source: Coding guidelines

backend/tests/mcp_server/test_delivery_todo_tools.py (1)

23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

SessionLocal stub lets the tools close the shared fixture session.

The delivery tools use with SessionLocal() as db:, so exiting the block calls close() on the very session the fixture owns (rolling back anything not yet committed and detaching instances). It works here because each tool commits, but it silently couples these tests to that invariant. A wrapper that yields the session without closing it is sturdier:

monkeypatch.setattr(
    delivery_tools, "SessionLocal", lambda: contextlib.nullcontext(test_db)
)
🤖 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/tests/mcp_server/test_delivery_todo_tools.py` around lines 23 - 25,
Update the patch_session_local fixture to replace the direct test_db return with
a non-closing context manager, such as contextlib.nullcontext(test_db), so
delivery_tools’ with SessionLocal() blocks do not close the shared fixture
session. Add the required contextlib import and preserve the existing
monkeypatch target.
backend/app/models/delivery.py (1)

139-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hoist the duplicated tags property to LoopNode.

CloudProject.tags and LoopItem.tags are byte-identical. Since both inherit LoopNode, define it once on the base (or a small mixin) so future changes to the metadata contract can't drift between the two.

As per coding guidelines, "extract shared logic instead of duplicating it".

♻️ Proposed refactor
 class LoopNode(Base):
     __tablename__ = "loop_items"
+
+    `@property`
+    def tags(self) -> list[str]:
+        """Tags stored inside the metadata JSON column."""
+        metadata = self.metadata_json
+        if not isinstance(metadata, dict):
+            return []
+        tags = metadata.get("tags")
+        if not isinstance(tags, list):
+            return []
+        return [str(tag) for tag in tags]

Then drop both subclass overrides.

Also applies to: 159-168

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

In `@backend/app/models/delivery.py` around lines 139 - 148, Move the shared tags
property implementation from CloudProject and LoopItem onto their common
LoopNode base (or a dedicated mixin), preserving its current metadata validation
and string-conversion behavior. Remove both subclass overrides so tags is
defined only once and inherited by both models.

Source: Coding guidelines

backend/app/mcp_server/tools/delivery.py (2)

24-48: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

MCP TODO payloads omit tags, and update_cloud_todo cannot set them.

This PR adds a first-class tag contract (LoopItem.tags, LoopItemCreate.tags, LoopItemUpdate.tags, REST responses), but _serialize_todo never emits tags and neither create_cloud_todo nor update_cloud_todo accepts them. Agents driving the board through MCP therefore can't read or maintain tags that the UI writes, and an agent-side "update everything I know" flow will look lossy.

Suggest adding tags to the serializer and threading an optional tags: list[str] | None through the create/update tools.

Also applies to: 351-379

🤖 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/mcp_server/tools/delivery.py` around lines 24 - 48, The MCP TODO
contract must preserve tags for both reads and writes. Update _serialize_todo to
include item.tags, and extend create_cloud_todo and update_cloud_todo with
optional tags: list[str] | None parameters, passing them through to the
corresponding LoopItemCreate and LoopItemUpdate payloads while preserving
existing behavior when omitted.

363-377: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

No way to clear nullable fields through update_cloud_todo.

Dropping every None means assignee_user_id, due_at, and parent_id can be set but never unset. If unassigning or clearing a due date is expected agent behavior, add explicit sentinels (e.g. clear_fields: list[str]) rather than relying on None.

🤖 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/mcp_server/tools/delivery.py` around lines 363 - 377, Update
update_cloud_todo’s field-building logic to support an explicit clear_fields
list for nullable fields, including assignee_user_id, due_at, and parent_id. Use
the sentinel to include those fields with None in LoopItemUpdate while
continuing to omit unspecified fields and preserving existing non-null updates.
backend/app/services/loop_items/service.py (1)

185-227: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Unknown ids in item_ids are silently dropped.

requested_ids filters out ids that are not lane members and only raises 422 when every id is unknown. A client sending a partially stale lane (e.g. an item already moved to another status) gets a 200 with a silently different order than requested, which is hard to debug from the board UI. Consider rejecting when len(requested_ids) != len(values.item_ids).

🤖 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 185 - 227, Update
reorder so it rejects any request containing an item ID that is not present in
the resolved lane, rather than silently filtering unknown IDs; validate
len(requested_ids) against len(values.item_ids) and raise the existing 422
HTTPException on mismatch, while preserving the current ordering behavior for
fully valid requests.
backend/app/mcp_server/server.py (1)

706-723: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving the project-space metadata onto McpAppSpec instead of branching on spec.name.

A root_metadata_extra (or protocol/capabilities) field on the spec keeps _build_root_metadata generic and avoids another if spec.name == ... branch the next time a server advertises capabilities.

🤖 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/mcp_server/server.py` around lines 706 - 723, The
_build_root_metadata function is coupled to the delivery server through a
spec.name branch; move project-space protocol, version, and capabilities
metadata into McpAppSpec via a root_metadata_extra or equivalent field, populate
it for delivery, and merge that field generically when building root metadata.
Preserve the existing metadata for specs without extras.
wework/src/features/todo/CloudMyWorkView.tsx (1)

331-331: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fixed two-column grid ignores the responsive breakpoints.

grid-cols-2 applies at every width, so the grouped view stays two-up below 768px. A responsive class (grid-cols-1 md:grid-cols-2) would follow the project's mobile/tablet/desktop rules.

As per coding guidelines: "Use mobile (<=767px), tablet (768px–1023px), and desktop (>=1024px) breakpoints … otherwise use responsive classes."

🤖 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/CloudMyWorkView.tsx` at line 331, Update the grid
container in the grouped view, identified by data-testid="my-work-groups", to
use one column by default and two columns from the md breakpoint onward.
Preserve the existing gap and other classes.

Source: Coding guidelines

wework/src/features/todo/CloudTodoWorkspace.tsx (3)

296-298: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

New user-facing copy is hardcoded instead of translated.

These newly added explanatory strings bypass i18n, unlike the new CloudMyWorkView in the same cohort which routes all copy through @/hooks/useTranslation. Consider adding these to the Wework common namespace for both en and zh-CN. Acknowledged that the surrounding dialogs already contain literals, so this can be a follow-up pass.

Based on the guideline "Use the local @/hooks/useTranslation wrapper for new Wework code" and "Add new copy to the appropriate Wework namespace in both the English and Simplified Chinese locale directories".

Also applies to: 392-394

🤖 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 296 - 298,
Replace the newly added hardcoded explanatory copy in CloudTodoWorkspace,
including the text at both referenced locations, with keys from the local
`@/hooks/useTranslation` wrapper. Add matching entries to the Wework common
namespace in both English and zh-CN locales, preserving the current wording and
rendered behavior.

Source: Coding guidelines


1011-1011: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant condition.

This branch only renders when projectView === 'board' (Line 983), so the ternary always yields boardParent. Same pattern at Line 1147: columnEmptyHints is a total Record over every status, so the && guard can never be false.

♻️ Suggested change
-                    onClick={() => openTodoCreation(projectView === 'board' ? boardParent : null)}
+                    onClick={() => openTodoCreation(boardParent)}
🤖 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` at line 1011, Remove the
redundant projectView ternary in the onClick handler and pass boardParent
directly to openTodoCreation. Also simplify the columnEmptyHints access near the
referenced second location by removing the unnecessary && guard, since the total
Record always provides a value for every status.

550-561: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Effect keyed on the whole selectedItem refetches on every drawer edit.

Only selectedItem.cloud_project_id is used, but the dependency is the object identity, so each onUpdatedsetSelectedItem(updated) re-runs the effect and refetches the entire foreign project's item list. Derive the id first and depend on that.

♻️ Suggested change
+  const detailProjectId =
+    selectedItem && selectedItem.cloud_project_id !== selectedProjectId
+      ? selectedItem.cloud_project_id
+      : null
   useEffect(() => {
-    if (!selectedItem || selectedItem.cloud_project_id === selectedProjectId) return
+    if (detailProjectId == null) return
     let active = true
-    void api.listLoopItems(selectedItem.cloud_project_id).then(response => {
+    void api.listLoopItems(detailProjectId).then(response => {
       if (active) setDetailItems(response.items)
     })
     return () => {
       active = false
     }
-  }, [api, selectedItem, selectedProjectId])
+  }, [api, detailProjectId])
🤖 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 550 - 561,
Update the drawer-items useEffect in CloudTodoWorkspace to derive the selected
item's cloud_project_id into a stable project-id value and use that value for
the guard, listLoopItems call, and dependency array instead of depending on the
whole selectedItem object.
wework/src/features/todo/CloudProjectManageView.tsx (1)

69-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tag registry CRUD has no test coverage.

persistRegistry/createTag/renameTag/deleteTag encode non-trivial rules (dedupe against allTags, version threading through projectVersion, cascading item updates) but the only assertion in this cohort's tests is the member row in CloudTodoWorkspace.test.tsx (Line 452). A focused test for create/rename/delete would protect the version-threading behavior.

As per coding guidelines: "Run focused tests before committing and broader tests when risk warrants it."

🤖 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 69 - 89,
添加针对 CloudProjectManageView 标签注册表 CRUD 的聚焦测试,覆盖 createTag、renameTag 和
deleteTag;验证 allTags 去重、projectVersion 传递及更新版本回写,并确认删除标签时关联待办项的级联更新行为。使用现有测试工具和
API mock,运行该聚焦测试后再提交。

Source: Coding guidelines

wework/src/features/todo/CloudTodoModal.tsx (1)

12-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider marking the dialog for assistive tech.

The restyled container is a plain section; adding role="dialog" aria-modal="true" with aria-label={title} (and an Escape handler) makes the overlay navigable for screen-reader and keyboard users.

♿ Proposed change
-      <section className="flex max-h-[calc(100vh-96px)] w-[480px] max-w-[calc(100vw-48px)] flex-col overflow-hidden rounded-2xl bg-background shadow-2xl">
+      <section
+        role="dialog"
+        aria-modal="true"
+        aria-label={title}
+        className="flex max-h-[calc(100vh-96px)] w-[480px] max-w-[calc(100vw-48px)] flex-col overflow-hidden rounded-2xl bg-background shadow-2xl"
+      >
🤖 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/CloudTodoModal.tsx` around lines 12 - 18, Update the
dialog container in CloudTodoModal by adding role="dialog", aria-modal="true",
and aria-label={title}; also handle Escape key presses to invoke onClose so
screen-reader and keyboard users can dismiss the overlay.
wework/src/features/todo/TodoEditor.tsx (2)

229-234: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

This file is at the 1000-line split threshold.

TodoEditor.tsx is 1023 lines and mixes draft persistence, attachment staging, collaborator management, and four rendered sections. Extracting the child/collaborator/local-execution/delivery sections (and the draft-storage helpers) into sibling modules keeps it under the limit and makes the create/edit branches easier to follow.

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

🤖 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 229 - 234, Split
TodoEditor.tsx into cohesive sibling modules, extracting the child,
collaborator, local-execution, and delivery sections plus draft-storage helpers
while preserving their existing behavior. Define explicit interfaces for the
extracted components/helpers, update TodoEditor to compose them, and remove any
dead code so the file remains below 1000 lines and its create/edit branches are
easier to follow.

Source: Coding guidelines


132-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New Wework UI copy is hardcoded Chinese instead of going through i18n. Both new components embed user-facing strings directly rather than resolving them through the local translation wrapper and locale namespaces.

  • wework/src/features/todo/TodoEditor.tsx#L132-L136: move the section headings, chip labels, footer hints, and error fallbacks to @/hooks/useTranslation keys.
  • wework/src/features/todo/TagEditor.tsx#L22-L22: move the 添加标签 placeholder default and the 移除标签 ${tag} aria-label to translation keys.

Add the corresponding entries to both wework/src/i18n/locales/en/** and wework/src/i18n/locales/zh-CN/**.

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 the English and Simplified Chinese locale directories".

🤖 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 132 - 136, Replace all
hardcoded user-facing copy in TodoEditor.tsx, including section headings, chip
labels, footer hints, and error fallbacks, with keys resolved through the local
`@/hooks/useTranslation` wrapper; add matching English and Simplified Chinese
entries under the appropriate namespaces in wework/src/i18n/locales/en/** and
wework/src/i18n/locales/zh-CN/**. In TagEditor.tsx, translate the 添加标签
placeholder default and 移除标签 ${tag} aria-label through the same wrapper and add
their corresponding entries to both locale directories.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc70f52c-6a91-4cc5-b8c7-9735e7b8f397

📥 Commits

Reviewing files that changed from the base of the PR and between 11a1a88 and a53df04.

⛔ Files ignored due to path filters (16)
  • demo/kanban-redesign/preview.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/files.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/index-detail.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/index-members.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/index-new-task.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/index-start-task.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/index.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/my-work.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/new-task.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/projects-new-project.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/projects.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/verify/my-work-calendar.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/verify/my-work-group.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/verify/my-work-list.png is excluded by !**/*.png
  • demo/kanban-redesign/shots/verify/my-work-timeline.png is excluded by !**/*.png
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (67)
  • backend/alembic/versions/20260724_b7c1d2e3f4a5_add_loop_items_deleted_at.py
  • backend/app/api/endpoints/deliveries.py
  • backend/app/mcp_server/server.py
  • backend/app/mcp_server/tool_registry.py
  • backend/app/mcp_server/tools/delivery.py
  • backend/app/models/delivery.py
  • backend/app/schemas/cloud_project.py
  • backend/app/schemas/delivery.py
  • backend/app/schemas/tagging.py
  • backend/app/services/cloud_projects/service.py
  • backend/app/services/loop_items/service.py
  • backend/app/services/runtime_work_service.py
  • backend/tests/api/test_cloud_projects_api.py
  • backend/tests/api/test_deliveries_api.py
  • backend/tests/mcp_server/test_delivery_todo_tools.py
  • backend/tests/mcp_server/test_delivery_tools.py
  • backend/tests/mcp_server/test_tool_registry.py
  • backend/tests/services/test_runtime_work_service.py
  • demo/cloud-space-mention/index.html
  • demo/kanban-redesign/assets/app.js
  • demo/kanban-redesign/assets/my-work.js
  • demo/kanban-redesign/assets/style.css
  • demo/kanban-redesign/files.html
  • demo/kanban-redesign/index.html
  • demo/kanban-redesign/my-work.html
  • demo/kanban-redesign/new-task.html
  • demo/kanban-redesign/projects.html
  • demo/kanban-redesign/shots/verify/shot-mywork.js
  • executor/src/agents/codex.rs
  • executor/src/agents/codex/tests.rs
  • wework/e2e/desktop/scenarios/cloud-space-mention.scenario.mjs
  • wework/package.json
  • wework/src/api/deliveries.ts
  • wework/src/api/local/localServices.test.ts
  • wework/src/api/local/localServices.ts
  • wework/src/components/chat/ChatInput.tsx
  • wework/src/components/chat/composer/CompactChatComposer.tsx
  • wework/src/components/chat/composer/ComposerMentionMenu.tsx
  • wework/src/components/chat/composer/ComposerTextarea.test.tsx
  • wework/src/components/chat/composer/ComposerTextarea.tsx
  • wework/src/components/chat/composer/ProjectChatComposer.tsx
  • wework/src/components/chat/composer/composerAutocomplete.ts
  • wework/src/components/chat/composer/composerMentionCandidates.test.ts
  • wework/src/components/chat/composer/composerMentionCandidates.ts
  • wework/src/components/chat/composer/composerTextareaTypes.ts
  • wework/src/components/chat/composer/useComposerMentionCandidates.ts
  • wework/src/components/layout/DesktopWorkbenchMain.tsx
  • wework/src/features/todo/CloudFilesView.tsx
  • wework/src/features/todo/CloudMyWorkCalendar.tsx
  • wework/src/features/todo/CloudMyWorkView.test.tsx
  • wework/src/features/todo/CloudMyWorkView.tsx
  • wework/src/features/todo/CloudProjectManageView.tsx
  • wework/src/features/todo/CloudProjectSettingsDialog.tsx
  • wework/src/features/todo/CloudTodoModal.tsx
  • wework/src/features/todo/CloudTodoWorkspace.test.tsx
  • wework/src/features/todo/CloudTodoWorkspace.tsx
  • wework/src/features/todo/TagEditor.tsx
  • wework/src/features/todo/TaskDescriptionEditor.tsx
  • wework/src/features/todo/TodoBindingPicker.tsx
  • wework/src/features/todo/TodoEditor.tsx
  • wework/src/features/todo/cloud-my-work-calendar.css
  • wework/src/features/todo/cloudMyWorkModel.ts
  • wework/src/features/todo/todoShared.test.ts
  • wework/src/features/todo/todoShared.ts
  • wework/src/i18n/locales/en/common.json
  • wework/src/i18n/locales/zh-CN/common.json
  • wework/src/test/setup.ts
💤 Files with no reviewable changes (1)
  • wework/src/features/todo/CloudProjectSettingsDialog.tsx

Comment on lines +1 to +15
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const base = 'file:///Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/my-work.html';
const out = '/Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/shots/verify/';
for (const view of ['group', 'list', 'calendar', 'timeline']) {
await page.goto(base);
await page.waitForTimeout(600);
await page.click(`[data-switch="${view}"]`);
await page.waitForTimeout(500);
await page.screenshot({ path: out + 'my-work-' + view + '.png' });
}
await browser.close();
})();

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 | ⚡ Quick win

Machine-specific absolute paths make this script unrunnable outside the author's machine.

base and out hardcode /Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/..., so this committed script only works on one developer's filesystem (and records their local layout). Derive both from the script location, and wrap in try/finally so a failed navigation doesn't leave a Chromium process behind.

♻️ Proposed fix
 const { chromium } = require('playwright-core');
+const path = require('path');
+const { pathToFileURL } = require('url');
 (async () => {
   const browser = await chromium.launch();
-  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
-  const base = 'file:///Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/my-work.html';
-  const out = '/Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/shots/verify/';
-  for (const view of ['group', 'list', 'calendar', 'timeline']) {
-    await page.goto(base);
-    await page.waitForTimeout(600);
-    await page.click(`[data-switch="${view}"]`);
-    await page.waitForTimeout(500);
-    await page.screenshot({ path: out + 'my-work-' + view + '.png' });
-  }
-  await browser.close();
+  try {
+    const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
+    const base = pathToFileURL(path.resolve(__dirname, '../../my-work.html')).href;
+    const out = __dirname;
+    for (const view of ['group', 'list', 'calendar', 'timeline']) {
+      await page.goto(base);
+      await page.click(`[data-switch="${view}"]`);
+      await page.waitForSelector(`.work-view.active[data-view="${view}"]`);
+      await page.screenshot({ path: path.join(out, `my-work-${view}.png`) });
+    }
+  } finally {
+    await browser.close();
+  }
 })();

If this was a one-off local verification helper, deleting it is also fine — happy to open an issue either way.

📝 Committable suggestion

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

Suggested change
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const base = 'file:///Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/my-work.html';
const out = '/Volumes/OuterHD/OuterIdeaProjects/weibo_wegent/github_wegent/demo/kanban-redesign/shots/verify/';
for (const view of ['group', 'list', 'calendar', 'timeline']) {
await page.goto(base);
await page.waitForTimeout(600);
await page.click(`[data-switch="${view}"]`);
await page.waitForTimeout(500);
await page.screenshot({ path: out + 'my-work-' + view + '.png' });
}
await browser.close();
})();
const { chromium } = require('playwright-core');
const path = require('path');
const { pathToFileURL } = require('url');
(async () => {
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const base = pathToFileURL(path.resolve(__dirname, '../../my-work.html')).href;
const out = __dirname;
for (const view of ['group', 'list', 'calendar', 'timeline']) {
await page.goto(base);
await page.click(`[data-switch="${view}"]`);
await page.waitForSelector(`.work-view.active[data-view="${view}"]`);
await page.screenshot({ path: path.join(out, `my-work-${view}.png`) });
}
} finally {
await browser.close();
}
})();
🤖 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 `@demo/kanban-redesign/shots/verify/shot-mywork.js` around lines 1 - 15,
Replace the machine-specific absolute base and output paths in the verification
script with paths derived from the script location, resolving the HTML file and
shots directory portably. Wrap the browser lifecycle and view iteration in
try/finally so chromium is always closed when navigation or screenshot
generation fails; alternatively remove this one-off helper if it is not intended
to remain committed.

Comment on lines +76 to +79
const rest = query.slice(label.length)
const separatorMatch = rest.match(/^[::]|\s+/)
if (!separatorMatch) return null
return rest.slice(separatorMatch[0].length)

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

Separator regex is only partially anchored, so whitespace anywhere in the remainder is treated as the separator.

In /^[::]|\s+/ the ^ applies only to the first alternative. For parseCloudProjectScopeQuery('项目空间abc def', labels) the second branch matches the space at index 3, and rest.slice(separatorMatch[0].length) then slices from index 0, returning 'bc def' instead of null — which also wrongly activates cloudProjectScopeActive in ComposerTextarea. The existing test only covers '项目空间abc' (no trailing whitespace), so it passes.

🐛 Anchor the full alternation
     const rest = query.slice(label.length)
-    const separatorMatch = rest.match(/^[::]|\s+/)
+    const separatorMatch = rest.match(/^(?:[::]|\s+)/)
     if (!separatorMatch) return null
📝 Committable suggestion

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

Suggested change
const rest = query.slice(label.length)
const separatorMatch = rest.match(/^[:]|\s+/)
if (!separatorMatch) return null
return rest.slice(separatorMatch[0].length)
const rest = query.slice(label.length)
const separatorMatch = rest.match(/^(?:[:]|\s+)/)
if (!separatorMatch) return null
return rest.slice(separatorMatch[0].length)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/chat/composer/composerAutocomplete.ts` around lines 76
- 79, Update the separator matching in parseCloudProjectScopeQuery to anchor the
entire alternation at the start of rest, so whitespace is accepted only when it
begins the remainder; queries with non-separator text followed by whitespace
must return null and must not activate cloudProjectScopeActive.

Comment on lines +831 to +856
const bindComposerCloudProject = useCallback(
(project: CloudProject, notice: string) => {
setCloudActionNotice(notice)
if (!currentRuntimeTask) {
setPendingCloudContext(project, null)
return
}
const api = services?.deliveryApi
if (!api) return
void api
.bindProjectTask(project.id, currentRuntimeTask, runtimeTaskTitle)
.then(() => {
setBoundCloudProject(project)
setBoundCloudItem(null)
setDeliveryItem(null)
})
.catch(cause => {
setTodoBindingError(
cause instanceof Error
? cause.message
: t('workbench.cloud_project_bind_failed', '关联项目空间失败')
)
})
},
[currentRuntimeTask, runtimeTaskTitle, services?.deliveryApi, setPendingCloudContext, t]
)

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

Success notice fires before (and independently of) the bind actually succeeding.

setCloudActionNotice(notice) runs first, so the "bound" toast also appears when services.deliveryApi is missing (early return at Line 839) and when bindProjectTask rejects — in the latter case the user gets a success toast plus an error toast. Move the notice into the success paths.

🐛 Proposed fix
   const bindComposerCloudProject = useCallback(
     (project: CloudProject, notice: string) => {
-      setCloudActionNotice(notice)
       if (!currentRuntimeTask) {
         setPendingCloudContext(project, null)
+        setCloudActionNotice(notice)
         return
       }
       const api = services?.deliveryApi
       if (!api) return
       void api
         .bindProjectTask(project.id, currentRuntimeTask, runtimeTaskTitle)
         .then(() => {
           setBoundCloudProject(project)
           setBoundCloudItem(null)
           setDeliveryItem(null)
+          setCloudActionNotice(notice)
         })
📝 Committable suggestion

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

Suggested change
const bindComposerCloudProject = useCallback(
(project: CloudProject, notice: string) => {
setCloudActionNotice(notice)
if (!currentRuntimeTask) {
setPendingCloudContext(project, null)
return
}
const api = services?.deliveryApi
if (!api) return
void api
.bindProjectTask(project.id, currentRuntimeTask, runtimeTaskTitle)
.then(() => {
setBoundCloudProject(project)
setBoundCloudItem(null)
setDeliveryItem(null)
})
.catch(cause => {
setTodoBindingError(
cause instanceof Error
? cause.message
: t('workbench.cloud_project_bind_failed', '关联项目空间失败')
)
})
},
[currentRuntimeTask, runtimeTaskTitle, services?.deliveryApi, setPendingCloudContext, t]
)
const bindComposerCloudProject = useCallback(
(project: CloudProject, notice: string) => {
if (!currentRuntimeTask) {
setPendingCloudContext(project, null)
setCloudActionNotice(notice)
return
}
const api = services?.deliveryApi
if (!api) return
void api
.bindProjectTask(project.id, currentRuntimeTask, runtimeTaskTitle)
.then(() => {
setBoundCloudProject(project)
setBoundCloudItem(null)
setDeliveryItem(null)
setCloudActionNotice(notice)
})
.catch(cause => {
setTodoBindingError(
cause instanceof Error
? cause.message
: t('workbench.cloud_project_bind_failed', '关联项目空间失败')
)
})
},
[currentRuntimeTask, runtimeTaskTitle, services?.deliveryApi, setPendingCloudContext, t]
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/components/layout/DesktopWorkbenchMain.tsx` around lines 831 -
856, Update bindComposerCloudProject so setCloudActionNotice is only called
after a successful bind or in the no-currentRuntimeTask path where the pending
context is set. Remove the initial unconditional call, and do not show the
success notice when deliveryApi is unavailable or bindProjectTask rejects.

Comment on lines +14 to +38
function makeItem(overrides: Partial<CloudMyWorkItem>): CloudMyWorkItem {
return {
id: 'WEG-1',
cloud_project_id: 1,
sequence_number: 1,
parent_id: null,
created_by_user_id: 1,
assignee_user_id: 1,
title: 'Cloud TODO',
description: '',
status: 'inbox',
priority: 'none',
due_at: null,
sort_order: 0,
current_delivery_id: null,
version: 1,
created_at: '2026-07-22T00:00:00Z',
updated_at: '2026-07-22T00:00:00Z',
completed_at: null,
project_key: 'WEG',
project_name: 'Wegent',
has_active_task: false,
...overrides,
}
}

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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm tags is required on CloudLoopItem and absent from this fixture
rg -nP -A22 'export interface CloudLoopItem' wework/src/api/deliveries.ts
rg -n 'tags' wework/src/features/todo/CloudMyWorkView.test.tsx || echo "no tags in fixture"
rg -n '"typecheck"|tsc' wework/package.json

Repository: wecode-ai/Wegent

Length of output: 1980


Add tags to the CloudMyWorkItem fixture.

CloudMyWorkItem extends CloudLoopItem, where tags: string[] is required. The ...overrides spread is typed as Partial<CloudMyWorkItem>, so it cannot satisfy this field; this fixture will fail the TypeScript build unless tags is provided directly in the returned object.

🐛 Proposed fix
     status: 'inbox',
     priority: 'none',
     due_at: null,
+    tags: [],
     sort_order: 0,
📝 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
function makeItem(overrides: Partial<CloudMyWorkItem>): CloudMyWorkItem {
return {
id: 'WEG-1',
cloud_project_id: 1,
sequence_number: 1,
parent_id: null,
created_by_user_id: 1,
assignee_user_id: 1,
title: 'Cloud TODO',
description: '',
status: 'inbox',
priority: 'none',
due_at: null,
sort_order: 0,
current_delivery_id: null,
version: 1,
created_at: '2026-07-22T00:00:00Z',
updated_at: '2026-07-22T00:00:00Z',
completed_at: null,
project_key: 'WEG',
project_name: 'Wegent',
has_active_task: false,
...overrides,
}
}
function makeItem(overrides: Partial<CloudMyWorkItem>): CloudMyWorkItem {
return {
id: 'WEG-1',
cloud_project_id: 1,
sequence_number: 1,
parent_id: null,
created_by_user_id: 1,
assignee_user_id: 1,
title: 'Cloud TODO',
description: '',
status: 'inbox',
priority: 'none',
due_at: null,
tags: [],
sort_order: 0,
current_delivery_id: null,
version: 1,
created_at: '2026-07-22T00:00:00Z',
updated_at: '2026-07-22T00:00:00Z',
completed_at: null,
project_key: 'WEG',
project_name: 'Wegent',
has_active_task: false,
...overrides,
}
}
🤖 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/CloudMyWorkView.test.tsx` around lines 14 - 38,
Update the makeItem fixture to include a default tags: string[] value in the
returned CloudMyWorkItem object before the overrides spread, satisfying the
required CloudLoopItem field while allowing callers to override it.

Comment on lines +14 to +18
const memberAvatarClasses = [
'bg-gradient-to-br from-indigo-400 to-indigo-500',
'bg-gradient-to-br from-emerald-400 to-emerald-500',
'bg-gradient-to-br from-amber-400 to-amber-500',
]

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 | ⚡ Quick win

Duplicate of memberAvatarClasses in todoShared.ts.

todoShared.ts already exports this exact array and CloudTodoWorkspace.tsx imports it from there — import it here too instead of re-declaring.

As per coding guidelines: "Before adding code, search for and reuse existing components, services, utilities, and patterns; extract shared logic instead of duplicating it."

♻️ Proposed fix
-const memberAvatarClasses = [
-  'bg-gradient-to-br from-indigo-400 to-indigo-500',
-  'bg-gradient-to-br from-emerald-400 to-emerald-500',
-  'bg-gradient-to-br from-amber-400 to-amber-500',
-]
+import { memberAvatarClasses } from './todoShared'

(place the import with the other module imports)

📝 Committable suggestion

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

Suggested change
const memberAvatarClasses = [
'bg-gradient-to-br from-indigo-400 to-indigo-500',
'bg-gradient-to-br from-emerald-400 to-emerald-500',
'bg-gradient-to-br from-amber-400 to-amber-500',
]
import { memberAvatarClasses } 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/CloudProjectManageView.tsx` around lines 14 - 18,
Remove the local memberAvatarClasses declaration in CloudProjectManageView.tsx
and import the existing memberAvatarClasses export from todoShared.ts alongside
the other module imports. Keep all usages unchanged and reuse the shared array.

Source: Coding guidelines

Comment on lines +91 to +147
async function renameTag(oldTag: string) {
const nextTag = renameValue.trim()
setRenamingTag(null)
if (!nextTag || nextTag === oldTag || tagBusy) return
if (allTags.includes(nextTag)) {
setError(`标签“${nextTag}”已存在`)
return
}
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(oldTag))
const renamed = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).map(tag => (tag === oldTag ? nextTag : tag)),
})
)
)
setItems(current => current.map(item => renamed.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(oldTag)) {
await persistRegistry(registryTags.map(tag => (tag === oldTag ? nextTag : tag)))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '重命名标签失败')
} finally {
setTagBusy(false)
}
}

async function deleteTag(target: string) {
const count = tagCounts.get(target) ?? 0
const hint = count > 0 ? `,并从 ${count} 个任务上移除` : ''
if (!window.confirm(`删除标签“${target}”${hint}?`) || tagBusy) return
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(target))
const stripped = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).filter(tag => tag !== target),
})
)
)
setItems(current => current.map(item => stripped.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(target)) {
await persistRegistry(registryTags.filter(tag => tag !== target))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '删除标签失败')
} finally {
setTagBusy(false)
}
}

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

Bulk tag rename/delete can leave items half-updated with no recovery.

Promise.all over updateLoopItem fans out one optimistic-concurrency write per affected item. If any rejects (e.g. a stale version because the 15s board poll or another member changed the item), the settled writes stay applied, setItems never runs for the successful ones, and only a generic message is shown — the registry rename is also skipped, so UI and server diverge. Prefer Promise.allSettled, apply the successes, refetch listLoopItems, and report how many failed.

Separately, deleteTag evaluates window.confirm(...) before checking tagBusy, so a confirm dialog appears even while another tag operation is in flight; check tagBusy first.

🛡️ Minimal fix for the `tagBusy` ordering
   async function deleteTag(target: string) {
+    if (tagBusy) return
     const count = tagCounts.get(target) ?? 0
     const hint = count > 0 ? `,并从 ${count} 个任务上移除` : ''
-    if (!window.confirm(`删除标签“${target}”${hint}?`) || tagBusy) return
+    if (!window.confirm(`删除标签“${target}”${hint}?`)) return
📝 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 renameTag(oldTag: string) {
const nextTag = renameValue.trim()
setRenamingTag(null)
if (!nextTag || nextTag === oldTag || tagBusy) return
if (allTags.includes(nextTag)) {
setError(`标签“${nextTag}”已存在`)
return
}
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(oldTag))
const renamed = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).map(tag => (tag === oldTag ? nextTag : tag)),
})
)
)
setItems(current => current.map(item => renamed.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(oldTag)) {
await persistRegistry(registryTags.map(tag => (tag === oldTag ? nextTag : tag)))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '重命名标签失败')
} finally {
setTagBusy(false)
}
}
async function deleteTag(target: string) {
const count = tagCounts.get(target) ?? 0
const hint = count > 0 ? `,并从 ${count} 个任务上移除` : ''
if (!window.confirm(`删除标签“${target}${hint}?`) || tagBusy) return
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(target))
const stripped = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).filter(tag => tag !== target),
})
)
)
setItems(current => current.map(item => stripped.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(target)) {
await persistRegistry(registryTags.filter(tag => tag !== target))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '删除标签失败')
} finally {
setTagBusy(false)
}
}
async function renameTag(oldTag: string) {
const nextTag = renameValue.trim()
setRenamingTag(null)
if (!nextTag || nextTag === oldTag || tagBusy) return
if (allTags.includes(nextTag)) {
setError(`标签“${nextTag}”已存在`)
return
}
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(oldTag))
const renamed = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).map(tag => (tag === oldTag ? nextTag : tag)),
})
)
)
setItems(current => current.map(item => renamed.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(oldTag)) {
await persistRegistry(registryTags.map(tag => (tag === oldTag ? nextTag : tag)))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '重命名标签失败')
} finally {
setTagBusy(false)
}
}
async function deleteTag(target: string) {
if (tagBusy) return
const count = tagCounts.get(target) ?? 0
const hint = count > 0 ? `,并从 ${count} 个任务上移除` : ''
if (!window.confirm(`删除标签“${target}${hint}?`)) return
setTagBusy(true)
setError(null)
try {
const affected = items.filter(item => (item.tags ?? []).includes(target))
const stripped = await Promise.all(
affected.map(item =>
api.updateLoopItem(item.id, {
version: item.version,
tags: (item.tags ?? []).filter(tag => tag !== target),
})
)
)
setItems(current => current.map(item => stripped.find(entry => entry.id === item.id) ?? item))
if (registryTags.includes(target)) {
await persistRegistry(registryTags.filter(tag => tag !== target))
}
} catch (cause) {
setError(cause instanceof Error ? cause.message : '删除标签失败')
} finally {
setTagBusy(false)
}
}
🤖 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 91 - 147,
Update renameTag and deleteTag to use Promise.allSettled for per-item updates,
apply successful results to local state, refetch via listLoopItems after the
batch, and report the number of failed updates while preserving registry
synchronization where appropriate. Also move the tagBusy guard in deleteTag
before evaluating window.confirm so no dialog appears during an active
operation.

Comment on lines +216 to +443
return (
<div className="min-h-0 flex-1 overflow-y-auto px-8 py-7">
<div className="mx-auto max-w-[960px]">
<h2 className="text-heading-md font-semibold">项目成员</h2>
<p className="mt-1 text-sm text-text-muted">
成员只能访问被授权的云项目、任务、共享文件和交付。
</p>

<div className="mt-6 overflow-hidden rounded-xl border border-border bg-background shadow-sm">
{members.map((member, index) => (
<div
key={member.user_id}
className="flex items-center gap-3 border-t border-border px-4 py-3 first:border-t-0"
data-testid={`cloud-project-member-${member.user_id}`}
>
<span
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold text-background',
memberAvatarClasses[index % memberAvatarClasses.length]
)}
>
{member.user_name.slice(0, 1)}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{member.user_name}</span>
<span className="block truncate text-xs text-text-muted">{member.email}</span>
</span>
{member.role === 'Owner' ? (
<span className="w-24 text-xs text-text-secondary">Owner</span>
) : (
<>
<select
data-testid={`cloud-project-member-role-${member.user_id}`}
value={member.role}
onChange={event =>
void updateMember(
member,
event.target.value as Exclude<CloudProjectMember['role'], 'Owner'>
)
}
className="h-8 w-24 rounded-lg border border-border bg-background px-1.5 text-xs outline-none focus:border-text-muted"
>
<option value="Maintainer">Maintainer</option>
<option value="Developer">Developer</option>
<option value="Reporter">Reporter</option>
</select>
<button
type="button"
data-testid={`cloud-project-member-remove-${member.user_id}`}
onClick={() => void removeMember(member)}
className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-text-muted hover:bg-muted hover:text-red-600"
aria-label={`移除 ${member.user_name}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
)}
</div>
))}
</div>

<div className="mt-8">
<h3 className="text-sm font-semibold">添加成员</h3>
<div className="mt-3 flex gap-2">
<label className="flex h-9 min-w-0 flex-1 items-center rounded-lg border border-border bg-background px-3 focus-within:border-text-muted">
<Search className="h-4 w-4 text-text-muted" />
<input
data-testid="cloud-member-search"
value={query}
onChange={event => setQuery(event.target.value)}
className="ml-2 min-w-0 flex-1 bg-transparent text-sm outline-none"
placeholder="搜索用户名或邮箱"
/>
</label>
<select
data-testid="cloud-member-role"
value={role}
onChange={event => setRole(event.target.value as CloudProjectMember['role'])}
className="h-9 rounded-lg border border-border bg-background px-2 text-sm outline-none focus:border-text-muted"
>
<option value="Maintainer">Maintainer</option>
<option value="Developer">Developer</option>
<option value="Reporter">Reporter</option>
</select>
</div>
{visibleResults.length > 0 && (
<div className="mt-2 overflow-hidden rounded-xl border border-border bg-background shadow-sm">
{visibleResults.map(user => (
<button
key={user.id}
type="button"
data-testid={`cloud-member-result-${user.id}`}
disabled={savingUserId !== null}
onClick={() => void addMember(user)}
className="flex w-full items-center border-t border-border px-4 py-2.5 text-left transition first:border-t-0 hover:bg-hover disabled:opacity-50"
>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">{user.user_name}</span>
<span className="block truncate text-xs text-text-muted">{user.email}</span>
</span>
<span className="rounded-md bg-text-primary px-2 py-1 text-xs text-background">
{savingUserId === user.id ? '添加中…' : '添加'}
</span>
</button>
))}
</div>
)}
{error && <p className="mt-2 text-xs text-destructive">{error}</p>}
</div>

<div className="mt-10">
<h2 className="text-heading-md font-semibold">标签管理</h2>
<p className="mt-1 text-sm text-text-muted">
标签用于区分任务类型(如产品需求、研发需求),新建后可在任务上选择,看板支持按标签筛选。
</p>

<div className="mt-4 flex gap-2">
<input
data-testid="cloud-project-tag-create-input"
value={newTag}
onChange={event => setNewTag(event.target.value)}
onKeyDown={event => {
if (event.key === 'Enter') {
event.preventDefault()
void createTag()
}
}}
placeholder="输入标签名称,如 产品需求"
maxLength={32}
className="h-9 min-w-0 flex-1 rounded-lg border border-border bg-background px-3 text-sm outline-none focus:border-text-muted"
/>
<button
type="button"
data-testid="cloud-project-tag-create-confirm"
disabled={!newTag.trim() || tagBusy}
onClick={() => void createTag()}
className="h-9 rounded-lg bg-text-primary px-3.5 text-sm font-medium text-background transition hover:opacity-90 disabled:opacity-50"
>
新建标签
</button>
</div>

<div className="mt-4 overflow-hidden rounded-xl border border-border bg-background shadow-sm">
{allTags.length === 0 && (
<p className="px-4 py-6 text-center text-xs text-text-muted">
还没有标签,先新建一个吧
</p>
)}
{allTags.map(tag => (
<div
key={tag}
data-testid={`cloud-project-tag-${tag}`}
className="flex items-center gap-3 border-t border-border px-4 py-2.5 first:border-t-0"
>
<Tag className="h-3.5 w-3.5 shrink-0 text-text-muted" />
{renamingTag === tag ? (
<span className="flex min-w-0 flex-1 items-center gap-2">
<input
data-testid={`cloud-project-tag-rename-input-${tag}`}
autoFocus
value={renameValue}
onChange={event => setRenameValue(event.target.value)}
onKeyDown={event => {
if (event.key === 'Enter') {
event.preventDefault()
void renameTag(tag)
}
if (event.key === 'Escape') setRenamingTag(null)
}}
maxLength={32}
className="h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-sm outline-none focus:border-text-muted"
/>
<button
type="button"
aria-label="确认重命名"
onClick={() => void renameTag(tag)}
className="flex h-6 w-6 items-center justify-center rounded-md text-text-muted hover:bg-muted hover:text-text-primary"
>
<Check className="h-3.5 w-3.5" />
</button>
<button
type="button"
aria-label="取消重命名"
onClick={() => setRenamingTag(null)}
className="flex h-6 w-6 items-center justify-center rounded-md text-text-muted hover:bg-muted hover:text-text-primary"
>
<X className="h-3.5 w-3.5" />
</button>
</span>
) : (
<>
<span className="min-w-0 flex-1 truncate text-sm font-medium">{tag}</span>
<span className="shrink-0 text-xs text-text-muted">
{tagCounts.get(tag) ?? 0} 个任务
</span>
<button
type="button"
data-testid={`cloud-project-tag-rename-${tag}`}
aria-label={`重命名标签 ${tag}`}
disabled={tagBusy}
onClick={() => {
setRenamingTag(tag)
setRenameValue(tag)
}}
className="flex h-7 w-7 items-center justify-center rounded-md text-text-muted hover:bg-muted hover:text-text-primary disabled:opacity-50"
>
<Pencil className="h-3.5 w-3.5" />
</button>
<button
type="button"
data-testid={`cloud-project-tag-delete-${tag}`}
aria-label={`删除标签 ${tag}`}
disabled={tagBusy}
onClick={() => void deleteTag(tag)}
className="flex h-7 w-7 items-center justify-center rounded-md text-text-muted hover:bg-muted hover:text-red-600 disabled:opacity-50"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</>
)}
</div>
))}
</div>
</div>
</div>
</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

New file ships entirely untranslated copy.

Every heading, placeholder, button label, aria-label and error string here is a hardcoded Chinese literal, while the sibling new file CloudMyWorkView.tsx routes all of its copy through @/hooks/useTranslation. This view will not localize.

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 the English and Simplified Chinese locale directories."

🤖 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 216 - 443,
The rendered copy in the CloudProjectManageView component is hardcoded in
Chinese and bypasses localization. Use the local useTranslation hook for every
heading, description, placeholder, button label, status text, aria-label, empty
state, and error string in this view, and add matching keys to the appropriate
Wework English and Simplified Chinese locale namespaces.

Source: Coding guidelines

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

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 | 🔴 Critical | ⚡ Quick win

Non-null assertion in the drag overlay can crash the workspace.

The 15s poll runs during a drag; if the dragged item disappears from items (deleted or soft-deleted by another member, or filtered out by a tag/search change), find returns undefined and the ! lets it through to TodoCardContent, which dereferences item.priority/item.updated_at and throws during render.

🛡️ Proposed fix
                     <DragOverlay dropAnimation={null}>
-                      {activeDragItemId ? (
-                        <div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
-                          <TodoCardContent
-                            item={items.find(item => item.id === activeDragItemId)!}
-                          />
-                        </div>
-                      ) : null}
+                      {(() => {
+                        const dragItem = items.find(item => item.id === activeDragItemId)
+                        if (!dragItem) return null
+                        return (
+                          <div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
+                            <TodoCardContent item={dragItem} />
+                          </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
item={items.find(item => item.id === activeDragItemId)!}
/>
</div>
) : null}
</DragOverlay>
<DragOverlay dropAnimation={null}>
{(() => {
const dragItem = items.find(item => item.id === activeDragItemId)
if (!dragItem) return null
return (
<div className="w-[272px] rotate-1 rounded-xl border border-border bg-background p-3 text-left shadow-lg">
<TodoCardContent item={dragItem} />
</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 1169 - 1177,
Remove the non-null assertion in the DragOverlay rendering and derive the
dragged item from items using activeDragItemId. Render TodoCardContent only when
the item exists, while preserving the existing overlay styling and null
rendering behavior when the item is deleted or filtered out.

Comment on lines +295 to +324
useEffect(() => {
if (editItemId == null || editProjectId == null) return
void Promise.all([
api.listDeliveries(editItemId),
api.listTaskBindings(editItemId),
api.listLoopItemAttachments(editItemId),
api.listLoopItemCollaborators(editItemId),
api.listCloudProjectMembers(editProjectId),
]).then(
([
deliveryResponse,
taskResponse,
attachmentResponse,
collaboratorResponse,
memberResponse,
]) => {
setDeliveries(deliveryResponse.items)
setTasks(taskResponse)
setAttachments(attachmentResponse)
setCollaborators(collaboratorResponse)
setProjectMembers(memberResponse)
}
)
}, [api, editItemId, editProjectId])

// Create mode only needs the member list for the assignee select.
useEffect(() => {
if (createProjectId == null) return
void api.listCloudProjectMembers(createProjectId).then(setProjectMembers)
}, [api, createProjectId])

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

Detail/member loads have no rejection handling and no cancellation.

Both effects call .then() without .catch(), so a failed listDeliveries/listCloudProjectMembers produces an unhandled rejection and the panel silently shows empty sections. They also lack an active flag, so a late response for a previously selected item can overwrite state after the item changes or the panel unmounts.

🛡️ Proposed fix
   useEffect(() => {
     if (editItemId == null || editProjectId == null) return
+    let active = true
     void Promise.all([
       api.listDeliveries(editItemId),
       api.listTaskBindings(editItemId),
       api.listLoopItemAttachments(editItemId),
       api.listLoopItemCollaborators(editItemId),
       api.listCloudProjectMembers(editProjectId),
     ]).then(
       ([
         deliveryResponse,
         taskResponse,
         attachmentResponse,
         collaboratorResponse,
         memberResponse,
       ]) => {
+        if (!active) return
         setDeliveries(deliveryResponse.items)
         setTasks(taskResponse)
         setAttachments(attachmentResponse)
         setCollaborators(collaboratorResponse)
         setProjectMembers(memberResponse)
       }
-    )
+    ).catch(cause => {
+      if (active) setSaveError(cause instanceof Error ? cause.message : '加载任务详情失败')
+    })
+    return () => {
+      active = false
+    }
   }, [api, editItemId, editProjectId])
 
   // Create mode only needs the member list for the assignee select.
   useEffect(() => {
     if (createProjectId == null) return
-    void api.listCloudProjectMembers(createProjectId).then(setProjectMembers)
+    let active = true
+    void api
+      .listCloudProjectMembers(createProjectId)
+      .then(members => {
+        if (active) setProjectMembers(members)
+      })
+      .catch(() => {
+        if (active) setProjectMembers([])
+      })
+    return () => {
+      active = false
+    }
   }, [api, createProjectId])
📝 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
useEffect(() => {
if (editItemId == null || editProjectId == null) return
void Promise.all([
api.listDeliveries(editItemId),
api.listTaskBindings(editItemId),
api.listLoopItemAttachments(editItemId),
api.listLoopItemCollaborators(editItemId),
api.listCloudProjectMembers(editProjectId),
]).then(
([
deliveryResponse,
taskResponse,
attachmentResponse,
collaboratorResponse,
memberResponse,
]) => {
setDeliveries(deliveryResponse.items)
setTasks(taskResponse)
setAttachments(attachmentResponse)
setCollaborators(collaboratorResponse)
setProjectMembers(memberResponse)
}
)
}, [api, editItemId, editProjectId])
// Create mode only needs the member list for the assignee select.
useEffect(() => {
if (createProjectId == null) return
void api.listCloudProjectMembers(createProjectId).then(setProjectMembers)
}, [api, createProjectId])
useEffect(() => {
if (editItemId == null || editProjectId == null) return
let active = true
void Promise.all([
api.listDeliveries(editItemId),
api.listTaskBindings(editItemId),
api.listLoopItemAttachments(editItemId),
api.listLoopItemCollaborators(editItemId),
api.listCloudProjectMembers(editProjectId),
]).then(
([
deliveryResponse,
taskResponse,
attachmentResponse,
collaboratorResponse,
memberResponse,
]) => {
if (!active) return
setDeliveries(deliveryResponse.items)
setTasks(taskResponse)
setAttachments(attachmentResponse)
setCollaborators(collaboratorResponse)
setProjectMembers(memberResponse)
}
).catch(cause => {
if (active) setSaveError(cause instanceof Error ? cause.message : '加载任务详情失败')
})
return () => {
active = false
}
}, [api, editItemId, editProjectId])
// Create mode only needs the member list for the assignee select.
useEffect(() => {
if (createProjectId == null) return
let active = true
void api
.listCloudProjectMembers(createProjectId)
.then(members => {
if (active) setProjectMembers(members)
})
.catch(() => {
if (active) setProjectMembers([])
})
return () => {
active = false
}
}, [api, createProjectId])
🤖 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 295 - 324, Update both
detail/member-loading useEffect blocks to track an active flag, set it false in
the cleanup function, and guard all state updates so late responses cannot
update state after selection changes or unmount. Add rejection handling to each
Promise chain, including the Promise.all flow for deliveries, tasks,
attachments, collaborators, and members, and the create-mode
listCloudProjectMembers call, using the existing error-reporting mechanism if
available.

@qdaxb
qdaxb merged commit bd0686d into wecode-ai:main Jul 25, 2026
20 of 22 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