refactor(wework): redesign cloud kanban workspace - #2215
Conversation
- 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
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis 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. ChangesDelivery backend and MCP
Cloud project-space mentions
Cloud TODO workspace
Interactive demos and executor support
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winFiles tab is missing a
data-testid.Its siblings expose
cloud-project-board-viewandcloud-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-testidvalues."🔧 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 winRollback undoes a status change that already committed on the server.
If
updateLoopItemsucceeds butreorderLoopItemsthen fails, thecatchrestorespreviousItems, 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 liftMount now issues 2×N requests to populate the project list.
Every project triggers both
listLoopItems(only.lengthis used) andlistCloudProjectMembers, andPromise.allfires 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 onlistCloudProjects(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 failedlistCloudProjects/listLoopItemsclearsloadingbut 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 winBullet and ordered list buttons now show the identical label “列表”.
Only the icon and
title/aria-labeldistinguish 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 winFix 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 asUse 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
CloudProjecttest fixtures omit the non-optionaltagsfield.CloudProject(wework/src/api/deliveries.tslines 72-84) declarestags: string[], so both annotated object literals should fail type checking.
wework/src/components/chat/composer/composerMentionCandidates.test.ts#L7-L18: addtags: []toCLOUD_PROJECT.wework/src/components/chat/composer/ComposerTextarea.test.tsx#L28-L39: addtags: []toWEBSITE_PROJECT(MOBILE_PROJECTinherits 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 winCheck for an existing trusted capability entry.
An untrusted
projectSpaceCapabilityentry is excluded at Line 1143 but still makescontext?.projectSpaceCapabilitytruthy, 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 winReject 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 winError 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 readserrormust 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 winDate-only
due_atis round-tripped through local time in both My Work views.TodoEditorsubmits the due date asYYYY-MM-DD, sodue_atarrives as a UTC-midnight instant; both views convert it vianew Date(...)in local time, which shifts the day backwards for negative UTC offsets. Normalize the date portion once incloudMyWorkModel.tsand share it.
wework/src/features/todo/CloudMyWorkView.tsx#L74-L83: parsedue_at.slice(0, 10)into a localDateindueDayOfinstead ofnew 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 eventstartso 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 winTransparent overlay
selecthas no visible focus indicator.The real control is
opacity-0and absolutely fills the styledspan, so keyboard focus produces no visible change on the wrapper. Add afocus-within:ring/border on thespanso 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 winRename 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-testidvalues."🔧 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 winAdd the ⌘K binding for the todo search toggle.
searchOpenis only toggled by the button click inwework/src/features/todo/CloudTodoWorkspace.tsx; there is no key listener for this component’s search dialog, so the⌘Kbadge 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 winBoard 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 labelsnoneas普通, whileCloudMyWorkView.tsx(Line 60-66) mapsmedium → 普通andnone → 无, so the same item reads differently in the two views. Extract a shared priority label map next topriorityBadgeClassesintodoShared.tsand 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 winTag input has no accessible name once a tag exists.
placeholderis dropped whentags.length > 0, leaving the input without any label. Add a staticaria-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 winStylelint 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 winDropping 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]?.idis the next card, not the item itself), andcurrentLaneIdsexcludesitemId, sobeforeIndexis-1and the card gets appended at the lane end — plus a spuriousreorderLoopItemsrequest frommoveItem(CloudTodoWorkspace.tsxLines 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 winPriority chip renders raw enum values.
Line 674 shows
priorityverbatim for anything butnone, so the chip readslow/medium/high/urgentwhile the select below uses 低/普通/高/紧急. It also labelsnoneas 普通, which the select maps tomedium. 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 winStaged attachment upload failures are swallowed silently.
Promise.allSettleddiscards 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 winKeep TipTap packages on one lockstep version policy.
@tiptap/coreand@tiptap/pmare pinned to2.27.2, but the extensions use^2.27.2; a future install can therefore mix different 2.x releases. Pin the extensions to2.27.2as 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 winDo 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 valueStylelint: missing blank line before the
font-sizedeclaration.🎨 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 winReuse
assets/style.cssinstead of re-declaring the design tokens here.This page duplicates the
:roottoken set and several component rules (.icon-btn,.badge,.btn-primary) that already exist indemo/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 winDead 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, soPOST /api/v1/cloud-projects,readJson(),createdProjectPayload, anddiagnostics()can never execute —diagnostics()will always reportnull. 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 winAlign the demo’s FullCalendar CDN pin with the app dependency version.
wework/package.jsondeclares@fullcalendar/core,@fullcalendar/daygrid, and@fullcalendar/reactat6.1.21, but the demo still loads the olderfullcalendar@6.1.15build, 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 winConsider replacing the nested ternary chains with per-row-kind lookup tables.
Icon,title, anddescriptionnow each nest 4-6 levels of ternaries overrow.kind, and adding another row kind means touching all three. ARecord<MentionMenuRow['kind'], { Icon; title; description? }>(with thecandidate/pathItemcases 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 winUse a named, typed tag validator.
Replace the inline lambda with a
@classmethodvalidator, matchingCloudProjectUpdate.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,
**/*.pyrequires “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
SessionLocalstub lets the tools close the shared fixture session.The delivery tools use
with SessionLocal() as db:, so exiting the block callsclose()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 winHoist the duplicated
tagsproperty toLoopNode.
CloudProject.tagsandLoopItem.tagsare byte-identical. Since both inheritLoopNode, 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 winMCP TODO payloads omit
tags, andupdate_cloud_todocannot set them.This PR adds a first-class tag contract (
LoopItem.tags,LoopItemCreate.tags,LoopItemUpdate.tags, REST responses), but_serialize_todonever emitstagsand neithercreate_cloud_todonorupdate_cloud_todoaccepts 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
tagsto the serializer and threading an optionaltags: list[str] | Nonethrough 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 valueNo way to clear nullable fields through
update_cloud_todo.Dropping every
Nonemeansassignee_user_id,due_at, andparent_idcan 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 onNone.🤖 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 valueUnknown ids in
item_idsare silently dropped.
requested_idsfilters 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 whenlen(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 valueConsider moving the project-space metadata onto
McpAppSpecinstead of branching onspec.name.A
root_metadata_extra(orprotocol/capabilities) field on the spec keeps_build_root_metadatageneric and avoids anotherif 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 valueFixed two-column grid ignores the responsive breakpoints.
grid-cols-2applies 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 valueNew user-facing copy is hardcoded instead of translated.
These newly added explanatory strings bypass i18n, unlike the new
CloudMyWorkViewin the same cohort which routes all copy through@/hooks/useTranslation. Consider adding these to the Weworkcommonnamespace for bothenandzh-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/useTranslationwrapper 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 valueRedundant condition.
This branch only renders when
projectView === 'board'(Line 983), so the ternary always yieldsboardParent. Same pattern at Line 1147:columnEmptyHintsis a totalRecordover 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 valueEffect keyed on the whole
selectedItemrefetches on every drawer edit.Only
selectedItem.cloud_project_idis used, but the dependency is the object identity, so eachonUpdated→setSelectedItem(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 winTag registry CRUD has no test coverage.
persistRegistry/createTag/renameTag/deleteTagencode non-trivial rules (dedupe againstallTags, version threading throughprojectVersion, cascading item updates) but the only assertion in this cohort's tests is the member row inCloudTodoWorkspace.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 winConsider marking the dialog for assistive tech.
The restyled container is a plain
section; addingrole="dialog" aria-modal="true"witharia-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 liftThis file is at the 1000-line split threshold.
TodoEditor.tsxis 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 winNew 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/useTranslationkeys.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/**andwework/src/i18n/locales/zh-CN/**.As per coding guidelines: "Use the local
@/hooks/useTranslationwrapper 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
⛔ Files ignored due to path filters (16)
demo/kanban-redesign/preview.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/files.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/index-detail.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/index-members.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/index-new-task.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/index-start-task.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/index.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/my-work.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/new-task.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/projects-new-project.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/projects.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/verify/my-work-calendar.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/verify/my-work-group.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/verify/my-work-list.pngis excluded by!**/*.pngdemo/kanban-redesign/shots/verify/my-work-timeline.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (67)
backend/alembic/versions/20260724_b7c1d2e3f4a5_add_loop_items_deleted_at.pybackend/app/api/endpoints/deliveries.pybackend/app/mcp_server/server.pybackend/app/mcp_server/tool_registry.pybackend/app/mcp_server/tools/delivery.pybackend/app/models/delivery.pybackend/app/schemas/cloud_project.pybackend/app/schemas/delivery.pybackend/app/schemas/tagging.pybackend/app/services/cloud_projects/service.pybackend/app/services/loop_items/service.pybackend/app/services/runtime_work_service.pybackend/tests/api/test_cloud_projects_api.pybackend/tests/api/test_deliveries_api.pybackend/tests/mcp_server/test_delivery_todo_tools.pybackend/tests/mcp_server/test_delivery_tools.pybackend/tests/mcp_server/test_tool_registry.pybackend/tests/services/test_runtime_work_service.pydemo/cloud-space-mention/index.htmldemo/kanban-redesign/assets/app.jsdemo/kanban-redesign/assets/my-work.jsdemo/kanban-redesign/assets/style.cssdemo/kanban-redesign/files.htmldemo/kanban-redesign/index.htmldemo/kanban-redesign/my-work.htmldemo/kanban-redesign/new-task.htmldemo/kanban-redesign/projects.htmldemo/kanban-redesign/shots/verify/shot-mywork.jsexecutor/src/agents/codex.rsexecutor/src/agents/codex/tests.rswework/e2e/desktop/scenarios/cloud-space-mention.scenario.mjswework/package.jsonwework/src/api/deliveries.tswework/src/api/local/localServices.test.tswework/src/api/local/localServices.tswework/src/components/chat/ChatInput.tsxwework/src/components/chat/composer/CompactChatComposer.tsxwework/src/components/chat/composer/ComposerMentionMenu.tsxwework/src/components/chat/composer/ComposerTextarea.test.tsxwework/src/components/chat/composer/ComposerTextarea.tsxwework/src/components/chat/composer/ProjectChatComposer.tsxwework/src/components/chat/composer/composerAutocomplete.tswework/src/components/chat/composer/composerMentionCandidates.test.tswework/src/components/chat/composer/composerMentionCandidates.tswework/src/components/chat/composer/composerTextareaTypes.tswework/src/components/chat/composer/useComposerMentionCandidates.tswework/src/components/layout/DesktopWorkbenchMain.tsxwework/src/features/todo/CloudFilesView.tsxwework/src/features/todo/CloudMyWorkCalendar.tsxwework/src/features/todo/CloudMyWorkView.test.tsxwework/src/features/todo/CloudMyWorkView.tsxwework/src/features/todo/CloudProjectManageView.tsxwework/src/features/todo/CloudProjectSettingsDialog.tsxwework/src/features/todo/CloudTodoModal.tsxwework/src/features/todo/CloudTodoWorkspace.test.tsxwework/src/features/todo/CloudTodoWorkspace.tsxwework/src/features/todo/TagEditor.tsxwework/src/features/todo/TaskDescriptionEditor.tsxwework/src/features/todo/TodoBindingPicker.tsxwework/src/features/todo/TodoEditor.tsxwework/src/features/todo/cloud-my-work-calendar.csswework/src/features/todo/cloudMyWorkModel.tswework/src/features/todo/todoShared.test.tswework/src/features/todo/todoShared.tswework/src/i18n/locales/en/common.jsonwework/src/i18n/locales/zh-CN/common.jsonwework/src/test/setup.ts
💤 Files with no reviewable changes (1)
- wework/src/features/todo/CloudProjectSettingsDialog.tsx
| 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(); | ||
| })(); |
There was a problem hiding this comment.
📐 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.
| 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.
| const rest = query.slice(label.length) | ||
| const separatorMatch = rest.match(/^[::]|\s+/) | ||
| if (!separatorMatch) return null | ||
| return rest.slice(separatorMatch[0].length) |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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] | ||
| ) |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.jsonRepository: 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.
| 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.
| 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', | ||
| ] |
There was a problem hiding this comment.
📐 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.
| 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
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 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
| <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> |
There was a problem hiding this comment.
🩺 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.
| <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.
| 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]) |
There was a problem hiding this comment.
🩺 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.
| 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.
Summary by CodeRabbit