Conversation
* feat: x-card * feat: x-card * feat: message * feat: x-card * feat: x-card * feat: x-card * feat: A2UI * feat: card * feat: x-card * feat: xcard * feat: a2ui * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: x-card * feat: update span * feat: x-card * feat: update span * feat: x-card * feat: x-card
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 Walkthrough🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
| const next = { ...obj }; | ||
| let cur: Record<string, any> = next; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {}; |
Check warning
Code scanning / CodeQL
Prototype-polluting assignment Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
In general, to fix prototype-polluting assignments when using dynamic keys, you either (a) prevent dangerous keys (__proto__, prototype, constructor) and malformed paths from being used, or (b) switch to data structures that are not prototype-based, like Map or Object.create(null). Here, the minimal, compatibility-preserving approach is to validate path segments and reject or ignore any paths that contain forbidden keys, while preserving the existing object shape and behavior for valid paths.
The best fix with minimal behavior change is to harden setValueByPath in packages/x-card/src/A2UI/utils.ts:
- Normalize
pathas before (strip leading/,split('/')), then check that the resulting array is non-empty and that each segment is a “safe” key. - Introduce a small helper (inside the same file) that returns
falsefor forbidden keys ('__proto__','prototype','constructor') andtrueotherwise. - If the overall path is invalid (empty) or any segment is unsafe, return the original object unchanged instead of performing any assignments.
- When creating intermediate containers, use a normal
{}as before to avoid affecting public API shape; we don’t need to change this.
This change is localized to setValueByPath, so all its callers (applyDataModelUpdateV09, handleAction in Card.tsx, and handleDataChange in Card.tsx, plus any v0.8 equivalents not shown) will automatically benefit from the added safety without further modifications. No new imports or external dependencies are required.
| @@ -9,6 +9,11 @@ | ||
| return parts.reduce((cur, key) => (cur != null ? cur[key] : undefined), obj as any); | ||
| } | ||
|
|
||
| /** Check if a path segment is safe to use as an object key (avoid prototype pollution) */ | ||
| function isSafePathSegment(segment: string): boolean { | ||
| return segment !== '__proto__' && segment !== 'constructor' && segment !== 'prototype'; | ||
| } | ||
|
|
||
| /** Write value to nested object by path (immutable), path format like /booking/selectedCoffee */ | ||
| export function setValueByPath( | ||
| obj: Record<string, any>, | ||
| @@ -16,13 +21,27 @@ | ||
| value: any, | ||
| ): Record<string, any> { | ||
| const parts = path.replace(/^\//, '').split('/'); | ||
|
|
||
| // Reject empty paths or unsafe segments to prevent prototype pollution | ||
| if ( | ||
| parts.length === 0 || | ||
| parts.some((segment) => !segment || !isSafePathSegment(segment)) | ||
| ) { | ||
| return obj; | ||
| } | ||
|
|
||
| const next = { ...obj }; | ||
| let cur: Record<string, any> = next; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {}; | ||
| cur = cur[parts[i]]; | ||
| const key = parts[i]; | ||
| const existing = cur[key]; | ||
| cur[key] = existing && typeof existing === 'object' && !Array.isArray(existing) | ||
| ? { ...existing } | ||
| : {}; | ||
| cur = cur[key]; | ||
| } | ||
| cur[parts[parts.length - 1]] = value; | ||
| const lastKey = parts[parts.length - 1]; | ||
| cur[lastKey] = value; | ||
| return next; | ||
| } | ||
|
|
| cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {}; | ||
| cur = cur[parts[i]]; | ||
| } | ||
| cur[parts[parts.length - 1]] = value; |
Check warning
Code scanning / CodeQL
Prototype-polluting assignment Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 days ago
In general terms, the problem is that setValueByPath writes to plain JavaScript objects using unvalidated dynamic property names. To fix this, we must ensure that no path segment can be interpreted as a special prototype-related key (__proto__, prototype, constructor), and ideally that the intermediate containers we create are not themselves inheriting from Object.prototype.
The minimal, non‑breaking fix here is to harden setValueByPath in packages/x-card/src/A2UI/utils.ts:
- Normalize the path into
partsas before. - Before using each segment as a property key, validate it:
- Reject or ignore dangerous segments (
'__proto__','prototype','constructor'). - We can either:
- Throw an error, or
- Return the original object unchanged.
To avoid changing functional behavior unexpectedly, we’ll choose to return the original object when an invalid segment is encountered, effectively treating the update as a no‑op.
- Reject or ignore dangerous segments (
- When creating nested objects, use
Object.create(null)so they are prototype‑less and cannot be used to polluteObject.prototypeeven if something slips through. - Use the same safeguards for the final property assignment.
This change is fully localized to setValueByPath, so callers in Card.v0.9.ts and Card.tsx do not need modifications. No new imports or external libraries are required; we only use Object.create(null) and some basic checks. The line with the vulnerable assignment (cur[parts[parts.length - 1]] = value;) will be replaced with logic that first validates the key and then assigns to a prototype‑less object.
| @@ -16,13 +16,41 @@ | ||
| value: any, | ||
| ): Record<string, any> { | ||
| const parts = path.replace(/^\//, '').split('/'); | ||
| const next = { ...obj }; | ||
|
|
||
| // Basic guard: if path is empty after normalization, just return original object | ||
| if (parts.length === 0 || (parts.length === 1 && parts[0] === '')) { | ||
| return obj; | ||
| } | ||
|
|
||
| // Reject dangerous keys that could lead to prototype pollution | ||
| const isUnsafeKey = (key: string): boolean => | ||
| key === '__proto__' || key === 'prototype' || key === 'constructor'; | ||
|
|
||
| if (parts.some((segment) => isUnsafeKey(segment))) { | ||
| // Silently ignore unsafe paths to avoid prototype pollution | ||
| return obj; | ||
| } | ||
|
|
||
| const next: Record<string, any> = { ...obj }; | ||
| let cur: Record<string, any> = next; | ||
|
|
||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {}; | ||
| cur = cur[parts[i]]; | ||
| const key = parts[i]; | ||
| const existing = cur[key]; | ||
|
|
||
| if (existing && typeof existing === 'object') { | ||
| // Clone existing object to maintain immutability | ||
| cur[key] = Array.isArray(existing) ? [...existing] : { ...existing }; | ||
| } else { | ||
| // Use a prototype-less object for newly created containers | ||
| cur[key] = Object.create(null); | ||
| } | ||
|
|
||
| cur = cur[key]; | ||
| } | ||
| cur[parts[parts.length - 1]] = value; | ||
|
|
||
| const lastKey = parts[parts.length - 1]; | ||
| cur[lastKey] = value; | ||
| return next; | ||
| } | ||
|
|
Deploying ant-design-x with
|
| Latest commit: |
c6122df
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2958e136.ant-design-x.pages.dev |
| Branch Preview URL: | https://main-merge-feature.ant-design-x.pages.dev |
There was a problem hiding this comment.
Code Review
This pull request introduces the @ant-design/x-card package, a dynamic card renderer supporting A2UI protocol versions 0.8 and 0.9, including catalog management and data binding utilities. Critical feedback identifies merge conflicts in package.json and SKILL.md that must be resolved. Technical improvements are suggested for the setValueByPath utility to prevent data corruption, adopting rimraf for cross-platform compatibility, and refactoring command filtering for better maintainability. Furthermore, exporting individual command interfaces is recommended for improved type safety, and the validation logic should be updated to distinguish between errors and warnings to ensure the accuracy of the validation status.
I am having trouble creating individual review comments. Click here to see my feedback.
package.json (3-7)
This file contains merge conflict markers (<<<<<<<, =======, >>>>>>>). Please resolve the conflict by choosing the correct version for the project.
packages/x-skill/skills/x-request/SKILL.md (3-7)
This file contains merge conflict markers. Please resolve the conflict to ensure the skill metadata is correct.
packages/x-card/src/A2UI/utils.ts (22)
This line can lead to unexpected behavior if cur[parts[i]] exists but is not a plain object (e.g., an array or a primitive). Spreading a non-object value can lead to data corruption. It's safer to check if the value is an object before spreading.
cur[parts[i]] = (cur[parts[i]] && typeof cur[parts[i]] === 'object' && !Array.isArray(cur[parts[i]])) ? { ...cur[parts[i]] } : {};
packages/x-card/package.json (44)
The clean script uses rm -rf, which is not cross-platform and will fail on Windows. Consider using a cross-platform tool like rimraf to ensure the script works for all contributors. You would need to add rimraf to your devDependencies.
"clean": "rimraf es lib coverage dist",
packages/x-card/src/A2UI/Card.tsx (186-194)
The filtering logic for commands is repetitive and could be simplified by extracting it into a helper function. This would make the code more readable and easier to maintain, especially if more command types are added in the future.
const getCommandSurfaceId = (cmd: any): string | undefined => {
if ('createSurface' in cmd) return cmd.createSurface.surfaceId;
if ('updateComponents' in cmd) return cmd.updateComponents.surfaceId;
if ('updateDataModel' in cmd) return cmd.updateDataModel.surfaceId;
if ('deleteSurface' in cmd) return cmd.deleteSurface.surfaceId;
if ('surfaceUpdate' in cmd) return cmd.surfaceUpdate.surfaceId;
if ('dataModelUpdate' in cmd) return cmd.dataModelUpdate.surfaceId;
if ('beginRendering' in cmd) return cmd.beginRendering.surfaceId;
return undefined;
};
const myCommands = commandQueue.filter((cmd) => getCommandSurfaceId(cmd) === id);
packages/x-card/src/A2UI/types/command_v0.8.ts (35-70)
To improve type safety and code clarity, it's beneficial to export the individual command interfaces. This enables using them in type guards and avoids brittle type assertions elsewhere in the code.
export interface SurfaceUpdateCommand {
surfaceUpdate: {
surfaceId: string;
components: ComponentWrapper_v0_8[];
};
}
// Command to update data model
export interface DataModelUpdateCommand {
dataModelUpdate: {
surfaceId: string;
contents: Array<{
key: string;
valueString?: string;
valueMap?: Array<{
key: string;
valueString: string;
}>;
}>;
};
}
// Command to begin rendering
export interface BeginRenderingCommand {
beginRendering: {
surfaceId: string;
root: string; // Root component ID
};
}
// Command to delete a surface
export interface DeleteSurfaceCommand {
deleteSurface: {
surfaceId: string;
};
}packages/x-card/src/A2UI/types/command_v0.9.ts (44-76)
For better type safety and to avoid type assertions in other parts of the codebase, it's a good practice to export the individual command interfaces (CreateSurfaceCommand, UpdateComponentsCommand, UpdateDataModelCommand, DeleteSurfaceCommand). This allows for more precise type guards and avoids casting to any.
export interface CreateSurfaceCommand {
version: 'v0.9';
createSurface: {
surfaceId: string;
catalogId: string; // 必需,组件目录 URL 或本地标识
};
}
// Command to update components on a surface
export interface UpdateComponentsCommand {
version: 'v0.9';
updateComponents: {
surfaceId: string;
components: BaseComponent_v0_9[];
};
}
// Command to update data model
export interface UpdateDataModelCommand {
version: 'v0.9';
updateDataModel: {
surfaceId: string;
path: string;
value: any;
};
}
// Command to delete a surface
export interface DeleteSurfaceCommand {
version: 'v0.9';
deleteSurface: {
surfaceId: string;
};
}packages/x-card/src/A2UI/utils.ts (51-89)
The current implementation mixes errors (like missing required fields) and warnings (like extra properties) in the same errors array. This makes the valid flag misleading, as it becomes false even for warnings. It's better to separate errors and warnings to provide more accurate validation results.
const errors: string[] = [];
const warnings: string[] = [];
// If no catalog, pass by default
if (!catalog || !catalog.components) {
return { valid: true, errors: [] };
}
// Check if component is defined in catalog
const componentDef = catalog.components[componentName];
if (!componentDef) {
errors.push(`Component "${componentName}" is not defined in catalog`);
return { valid: false, errors };
}
// Check required fields
const requiredFields = componentDef.required || [];
for (const field of requiredFields) {
if (!(field in componentProps)) {
errors.push(`Missing required field "${field}" for component "${componentName}"`);
}
}
// Check if properties are defined in schema (warning level, does not block rendering)
if (componentDef.properties) {
const definedProps = Object.keys(componentDef.properties);
const actualProps = Object.keys(componentProps).filter(
(key) => !['id', 'children', 'component'].includes(key),
);
for (const prop of actualProps) {
if (!definedProps.includes(prop)) {
warnings.push(
`Warning: Property "${prop}" is not defined in catalog for component "${componentName}"`,
);
}
}
}
return { valid: errors.length === 0, errors: [...errors, ...warnings] };|
This comment is automatically generated by the x-markdown performance CI. |
size-limit report 📦
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1838 +/- ##
==========================================
+ Coverage 97.42% 97.46% +0.04%
==========================================
Files 149 158 +9
Lines 5044 5485 +441
Branches 1438 1605 +167
==========================================
+ Hits 4914 5346 +432
- Misses 128 137 +9
Partials 2 2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
This comment is automatically generated by the x-markdown performance CI. |
There was a problem hiding this comment.
Actionable comments posted: 3
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (18)
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-form.json-8-64 (1)
8-64:⚠️ Potential issue | 🟠 MajorSchema 缺少
required,会导致关键字段未被强制校验。当前
RegistrationForm和SuccessCard里虽然声明了component.const,但未在对象级required中声明,校验器可接受不含component的对象,削弱 catalog 约束。🩹 建议最小修复
"RegistrationForm": { "type": "object", + "required": ["component"], "properties": { "component": { "const": "RegistrationForm" }, @@ "SuccessCard": { "type": "object", + "required": ["component"], "properties": { "component": { "const": "SuccessCard" },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-form.json` around lines 8 - 64, The schema objects "RegistrationForm" and "SuccessCard" are missing object-level required arrays so critical fields (e.g., component) aren't enforced; update the "RegistrationForm" schema to add a "required" list including "component" and other mandatory fields like "step" and "status" as appropriate, and update the "SuccessCard" schema to add a "required" list including "component" and required display fields like "username" and "email" (adjust lists to match which fields must be mandatory), ensuring the top-level object definitions for RegistrationForm and SuccessCard include these required arrays.packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx-701-706 (1)
701-706:⚠️ Potential issue | 🟠 Major流式文案在结束后会被直接移除。
Line 734-735 把非
RUNNING状态的时间戳置成了0并过滤掉,所以欢迎文案一结束就会从列表里消失。这里已经有 hook 返回的稳定时间戳可用,直接复用就够了。💡 建议修改
const { text: textHeader, streamStatus: streamStatusHeader, + timestamp: textHeaderTimestamp, run: runHeader, reset: resetHeader, } = useStreamText(contentHeader); @@ content: { - texts: [ - { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 }, - ].filter((item) => item.timestamp !== 0), + texts: textHeader ? [{ text: textHeader, timestamp: textHeaderTimestamp }] : [], card, } as ContentType,Also applies to: 734-735
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx` around lines 701 - 706, The stream text is removed when its status ends because the code forces non-RUNNING timestamps to 0 and then filters them out; update the logic that sets timestamps (the block handling streamStatusHeader/runHeader for contentHeader) to reuse the stable timestamp provided by useStreamText (the hook that returns textHeader, streamStatusHeader, runHeader, resetHeader) instead of assigning 0 for non-RUNNING states so the welcome copy remains in the list after streaming ends.packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx-598-603 (1)
598-603:⚠️ Potential issue | 🟠 Major流式文案在结束后会被直接移除。
Line 633-634 把非
RUNNING状态的时间戳置成了0并过滤掉,所以欢迎文案一结束就会从列表里消失。这里已经有 hook 返回的稳定时间戳可用,没必要每次 render 再取一次Date.now().💡 建议修改
const { text: textHeader, streamStatus: streamStatusHeader, + timestamp: textHeaderTimestamp, run: runHeader, reset: resetHeader, } = useStreamText(contentHeader); @@ content: { - texts: [ - { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 }, - ].filter((item) => item.timestamp !== 0), + texts: textHeader ? [{ text: textHeader, timestamp: textHeaderTimestamp }] : [], card, } as ContentType,Also applies to: 632-634
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx` around lines 598 - 603, The stream text is being removed after finishing because the render sets non-RUNNING timestamps to 0 and filters them out using a fresh Date.now(); instead, use the stable timestamp provided by the hook returned values (useStreamText -> textHeader, streamStatusHeader, runHeader, resetHeader) rather than calling Date.now() on each render, and stop zeroing timestamps for non-RUNNING states so completed stream entries remain in the list; update the logic that currently sets timestamp = (status !== RUNNING ? 0 : Date.now()) to reference the hook's stable timestamp value and only change it when runHeader/resetHeader indicate a new stream.packages/x/docs/x-card/demo/A2UI_v0.9/catalog-nested.json-79-95 (1)
79-95:⚠️ Potential issue | 🟠 Major这两处
oneOf会把合法的{ path }绑定判成无效。普通对象分支和 path-binding 分支的形状重叠,
{ path: '...' }会同时命中两支;按 JSON Schema 语义,oneOf必须“恰好一支”匹配,所以这里的绑定值会校验失败。要么把普通对象分支收窄,要么改成anyOf.💡 最小修正示例
"selectedFile": { "description": "Currently selected file.", - "oneOf": [ + "anyOf": [ { "type": "object" }, @@ "file": { "description": "File detail object.", - "oneOf": [ + "anyOf": [ { "type": "object", "properties": {Also applies to: 130-154
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-nested.json` around lines 79 - 95, 字段 selectedFile 使用了 oneOf 包含一个通用 object 分支和一个带 path 的 object 分支,这两者形状重叠导致诸如 { "path": "..." } 同时命中两支而触发 oneOf 的“恰好一支”校验失败;请在 catalog-nested.json 中把对应的 oneOf 改为 anyOf(或将第一个通用 object 分支收窄以与带 path 的分支互斥),对文件中第二处相同结构(原评论提到的 130-154 区域)做同样修改,确保使用的 schema 关键字为 anyOf 或调整分支属性以消除重叠。packages/x-card/src/A2UI/Box.tsx-20-30 (1)
20-30:⚠️ Potential issue | 🟠 Major重置命令时请同步清空
surfaceCatalogMap。现在只把
processedCommandsCount归零。父组件把commands清空或替换成更短的新队列后,旧的surfaceId -> catalogId映射还会继续留在 context 里,和这里注释里的 “reprocess from scratch” 语义不一致。💡 建议修改
useEffect(() => { // commands was cleared or reset — reset the counter and bail out if (!commands || commands.length === 0) { processedCommandsCount.current = 0; + setSurfaceCatalogMap((prev) => (prev.size === 0 ? prev : new Map())); return; } // commands array was replaced with a shorter one (e.g. reset by parent) — reset counter and reprocess from scratch if (commands.length < processedCommandsCount.current) { processedCommandsCount.current = 0; + setSurfaceCatalogMap((prev) => (prev.size === 0 ? prev : new Map())); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/Box.tsx` around lines 20 - 30, When resetting processedCommandsCount in the useEffect (the branches that check !commands || commands.length === 0 and commands.length < processedCommandsCount.current), also clear the surfaceCatalogMap so stale surfaceId -> catalogId mappings are removed; locate the useEffect handling commands and add logic to either call surfaceCatalogMap.clear() (or reset the context/store that holds surfaceCatalogMap) in both the "commands cleared" and "shorter commands array" branches to ensure reprocessing truly starts from scratch.packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx-617-627 (1)
617-627:⚠️ Potential issue | 🟠 Major
Reload没有清掉上一轮命令历史。
sessionKey会让XCard.Box重新挂载并从头消费整条commandQueue。这里继续保留旧 queue,只会让每次 reload 都把之前 session 的命令再回放一遍,处理量会持续累积。💡 建议修改
const handleReload = useCallback(() => { resetHeader(); - const deleteCommands: XAgentCommand_v0_9[] = [ - { version: 'v0.9', deleteSurface: { surfaceId: 'filter' } }, - ]; - setCommandQueue((prev) => [...prev, ...deleteCommands]); + setCommandQueue([]); setCard([]); - setTimeout(() => { - setSessionKey((prev) => prev + 1); - }, 50); + setSessionKey((prev) => prev + 1); }, [resetHeader]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/filter-search.tsx` around lines 617 - 627, handleReload currently leaves the previous commandQueue intact so when sessionKey causes XCard.Box to remount the old commands are replayed; update handleReload to clear or replace the queue instead of appending deleteCommands — e.g. call setCommandQueue to set the queue to just the deleteCommands (or an empty array then push deleteCommands) before setSessionKey, keep the existing calls to resetHeader and setCard, and ensure you reference handleReload, setCommandQueue, commandQueue, sessionKey, setSessionKey and setCard when making the change.packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx-720-728 (1)
720-728:⚠️ Potential issue | 🟠 Major
Reload没有清掉上一轮命令历史。
sessionKey会让XCard.Box重新挂载并从头消费整条commandQueue。这里继续保留旧 queue,只会让每次 reload 都把之前 session 的命令再回放一遍,处理量会持续累积。💡 建议修改
const handleReload = useCallback(() => { resetHeader(); - const deleteCommands: XAgentCommand_v0_8[] = [{ deleteSurface: { surfaceId: 'filter' } }]; - setCommandQueue((prev) => [...prev, ...deleteCommands]); + setCommandQueue([]); setCard([]); - setTimeout(() => { - setSessionKey((prev) => prev + 1); - }, 50); + setSessionKey((prev) => prev + 1); }, [resetHeader]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/filter-search.tsx` around lines 720 - 728, The reload handler handleReload currently appends deleteCommands to the existing commandQueue so when sessionKey forces XCard.Box to remount it replays previous sessions; change handleReload to replace/clear the queue (e.g., call setCommandQueue to set only the deleteSurface command or an empty array before remount) so old commands are not replayed; keep resetHeader, setCard([]) and the sessionKey bump as-is but ensure setCommandQueue is invoked with the fresh array (not prev => [...prev, ...]) to fully clear prior history.packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx-994-999 (1)
994-999:⚠️ Potential issue | 🟠 Major流式结束后把欢迎文案过滤掉了。
timestamp在RUNNING之外被写成0,后面的filter会直接删掉这条文本,所以状态一旦变成FINISHED,header 就消失了。这里应该复用useStreamText返回的固定时间戳,而不是每次 render 临时写Date.now()/0。🛠️ 建议修改
const { text: textHeader, streamStatus: streamStatusHeader, + timestamp: timestampHeader, run: runHeader, reset: resetHeader, } = useStreamText(contentHeader); @@ texts: [ - { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 }, + { text: textHeader, timestamp: timestampHeader }, ].filter((item) => item.timestamp !== 0),Also applies to: 1047-1049
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx` around lines 994 - 999, The header text is being filtered out after streaming because you write timestamp as 0/Date.now() on each render instead of reusing the stable timestamp returned by useStreamText; update the code that constructs the header item to use the timestamp value provided by useStreamText (the same object that yields textHeader/streamStatusHeader/runHeader/resetHeader) rather than computing Date.now() or 0 per render so the item keeps its original timestamp and isn't dropped when streamStatus moves from RUNNING to FINISHED; apply the same change to the other similar block that uses useStreamText at the corresponding spot.packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx-950-955 (1)
950-955:⚠️ Potential issue | 🟠 Major流式结束后把欢迎文案过滤掉了。
这里把 header 的
timestamp绑成了Date.now()/0,而后面的filter又会剔除0。结果就是一旦状态进入FINISHED,欢迎文案会直接消失。应该改用useStreamText返回的固定时间戳。🛠️ 建议修改
const { text: textHeader, streamStatus: streamStatusHeader, + timestamp: timestampHeader, run: runHeader, reset: resetHeader, } = useStreamText(contentHeader); @@ texts: [ - { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 }, + { text: textHeader, timestamp: timestampHeader }, ].filter((item) => item.timestamp !== 0),Also applies to: 1000-1002
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx` around lines 950 - 955, The header welcome text is being filtered out because you bind its timestamp to Date.now()/0 instead of the stable timestamp returned by useStreamText; update the destructuring of useStreamText to also capture the returned timestamp (e.g., timestamp: timestampHeader) and replace any hardcoded Date.now()/0 used when building contentHeader with that timestampHeader; apply the same change for the other occurrences mentioned (lines ~1000-1002) so the filter keeps the welcome copy after streamStatus becomes FINISHED.packages/x-card/src/A2UI/Card.tsx-172-176 (1)
172-176:⚠️ Potential issue | 🟠 Major不要把已渲染 surface 的根节点写死成
root。这里已经保存了
beginRendering.root/pendingRenderRef,但后续surfaceUpdate仍然固定getById('root')。只要某个 surface 的根 id 不是root,或者先收到beginRendering再收到surfaceUpdate,更新就会被静默丢掉。当前同一个 PR 里的checkout-success就已经用了success-card作为根节点。🛠️ 建议修改方向
+ const renderedRootIdRef = useRef<string | null>(null); + if ('surfaceUpdate' in cmd) { const nodeTree = transformerRef.current!.transform(cmd.surfaceUpdate.components, 'v0.8'); pendingNodeTreeRef.current = nodeTree; - // If already rendered, update directly - if (hasRenderedRef.current) { - const rootNodeFromCache = transformerRef.current!.getById('root'); + const activeRootId = renderedRootIdRef.current ?? pendingRenderRef.current?.root; + if (activeRootId && (hasRenderedRef.current || pendingRenderRef.current)) { + const rootNodeFromCache = transformerRef.current!.getById(activeRootId); if (rootNodeFromCache) { nextRootNode = rootNodeFromCache; + pendingRenderRef.current = null; + hasRenderedRef.current = true; hasRootNodeChange = true; } } } @@ if ('beginRendering' in cmd) { const { root } = cmd.beginRendering; + renderedRootIdRef.current = root; const nodeTree = transformerRef.current!.getById(root); @@ if ('deleteSurface' in cmd) { + renderedRootIdRef.current = null; nextRootNode = null;Also applies to: 258-269, 280-303
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/Card.tsx` around lines 172 - 176, The code is hardcoding the rendered surface root as 'root', causing updates to be dropped when the actual surface id differs; change surfaceUpdate handling to read the currently pending root id from pendingRenderRef (set by beginRendering) and use that id when calling getById instead of the literal 'root'. Specifically, update the logic that uses getById('root') in the surfaceUpdate flow to reference pendingRenderRef.current?.root (fallback to the previous behavior only if null), and ensure pendingNodeTreeRef and hasRenderedRef are used consistently so updates apply to the correct surface root for the component tree stored by beginRendering.packages/x-card/src/A2UI/Card.tsx-327-349 (1)
327-349:⚠️ Potential issue | 🟠 Major外层事件 payload 还没有走声明式 action context 解析。
现在这里直接把组件原始
context透传给上层,action.context/action.event.context里的{ path }和字面量配置只参与了 dataModel 写回,没有参与最终事件 payload。这样一来,声明式 action context 对上层监听者是失效的。建议在应用dataUpdates之后,用resolveActionContextV08/V09基于newDataModel生成 payload,再和组件上下文合并。🛠️ 建议修改
- // Report event to upper layer - onAction?.({ - name, - surfaceId: id, - context: { ...context }, - }); + const resolvedActionContext = + commandVersion === 'v0.9' + ? (resolveActionContextV09(actionConfig, newDataModel) ?? {}) + : (resolveActionContextV08(actionConfig, newDataModel) ?? {}); + + // Report event to upper layer + onAction?.({ + name, + surfaceId: id, + context: { ...resolvedActionContext, ...context }, + });-import { resolvePropsV08, extractDataUpdatesV08, applyDataModelUpdateV08 } from './Card.v0.8'; +import { + resolvePropsV08, + resolveActionContextV08, + extractDataUpdatesV08, + applyDataModelUpdateV08, +} from './Card.v0.8'; @@ -import { resolvePropsV09, extractDataUpdatesV09, applyDataModelUpdateV09 } from './Card.v0.9'; +import { + resolvePropsV09, + resolveActionContextV09, + extractDataUpdatesV09, + applyDataModelUpdateV09, +} from './Card.v0.9';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/Card.tsx` around lines 327 - 349, The current handleAction forwards the original component context to onAction and ignores declaration-based action context; after applying dataUpdates and producing newDataModel (using setValueByPath and setDataModel), call the appropriate resolver (resolveActionContextV09 when commandVersion === 'v0.9', otherwise resolveActionContextV08) with actionConfig and newDataModel to produce the resolved action context, then merge that resolved context into the payload you send to onAction (e.g., context: { ...context, ...resolvedContext }) so that declaration-based { path } and literal configs participate in the final event payload.packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx-114-121 (1)
114-121:⚠️ Potential issue | 🟠 Major这里也需要稳定 key
shop → checkout-success时如果继续用index,React 会复用旧的XCard.Card实例,而Card内部状态不会因为id改变自动清空。contentList的节点都有timestamp和id属性,应改用这些稳定的值作为 key。🛠️ 建议修改
- return contentList.map((node, index) => { + return contentList.map((node) => { if ('text' in node && node.text) { - return <XMarkdown key={index}>{node.text}</XMarkdown>; + return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>; } if ('id' in node && node.id) { - return <XCard.Card key={index} id={node.id} />; + return <XCard.Card key={`card-${node.id}`} id={node.id} />; } return null; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/multi-card-sync.tsx` around lines 114 - 121, The map callback uses index for React keys which can cause XCard.Card instances to be reused incorrectly; update the key usage in the contentList.map (the XMarkdown and XCard.Card returns) to use stable identifiers from the node (prefer node.id for card nodes and node.timestamp or node.id for text nodes) instead of index so React will remount when id/timestamp changes.packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx-115-122 (1)
115-122:⚠️ Potential issue | 🟠 Major不要用
index作为 key,使用稳定的节点标识符。这里会在
shop → checkout-success切换时复用旧的XMarkdown和XCard.Card实例,导致新 surface 继承旧 surface 的内部状态。由于TextNode和CardNode都包含timestamp属性,CardNode还包含id属性,应改为稳定 key 后,surface 替换才会正确 remount 组件。🛠️ 建议修改
- return contentList.map((node, index) => { + return contentList.map((node) => { if ('text' in node && node.text) { - return <XMarkdown key={index}>{node.text}</XMarkdown>; + return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>; } if ('id' in node && node.id) { - return <XCard.Card key={index} id={node.id} />; + return <XCard.Card key={`card-${node.id}`} id={node.id} />; } return null; });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/multi-card-sync.tsx` around lines 115 - 122, Replace the unstable index key used in the contentList.map render with a stable node identifier so React will remount surfaces correctly: in the mapping that returns XMarkdown and XCard.Card, use node.timestamp for text nodes (TextNode) and node.id for card nodes (CardNode), falling back to node.timestamp if id is missing; update the key props on XMarkdown and XCard.Card accordingly (locate the contentList.map block that renders XMarkdown and XCard.Card).packages/x/docs/x-card/demo/A2UI_v0.8/basic.tsx-28-35 (1)
28-35:⚠️ Potential issue | 🟠 Major使用稳定的 key 替换 index,避免状态复用问题。
当
booking切换到result等场景时,contentList可能会重新排序或变化。使用index作为 key 会导致 React 复用旧的XCard.Card实例,而该实例维护的transformer、dataModel和commandVersion等状态不会被清理,容易将上一个 surface 的内部状态泄露到新的 surface 中。建议改用基于node.id和node.timestamp的稳定 key,并在表单提交后重点测试 surface 的切换是否正确清空了前一个 surface 的状态。🛠️ 建议修改
- return contentList.map((node, index) => { + return contentList.map((node) => { if ('text' in node && node.text) { - return <XMarkdown key={index}>{node.text}</XMarkdown>; + return <XMarkdown key={`text-${node.timestamp}`}>{node.text}</XMarkdown>; } if ('id' in node && node.id) { - return <XCard.Card key={index} id={node.id} />; + return <XCard.Card key={`card-${node.id}`} id={node.id} />; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/basic.tsx` around lines 28 - 35, Replace unstable index keys to prevent React from reusing component instances: in the contentList mapping that returns <XMarkdown> and <XCard.Card> (the map over contentList producing XMarkdown and XCard.Card), stop using index as the key and instead build a stable key from node properties (e.g. use node.id and node.timestamp for cards and node.id or a stable id for text nodes); ensure XCard.Card receives a key derived from node.id+node.timestamp so switching between surfaces like booking→result unmounts the previous XCard.Card and clears its transformer/dataModel/commandVersion state, then run the form-submit surface switch test to confirm old state is not carried over.packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx-83-107 (1)
83-107:⚠️ Potential issue | 🟠 Major最后一步会把同一个
submit_step发两次。
handleNext()在currentStep === 1时先发一次普通事件,再调用handleSubmit()发一次带submit: true的同名事件。当前App.handleAction()会因此先弹一次中间态提示,再执行真正提交;如果后面把这个事件接到请求或埋点上,也会得到双份副作用。把事件派发放进分支里,最后一步只保留一次提交事件。🛠️ 一个直接的改法
+ const emitAction = (values: any, extra: Record<string, any> = {}) => { + if (!action?.event?.name) { + return; + } + const context: Record<string, any> = {}; + if (action.event.context) { + Object.keys(action.event.context).forEach((key) => { + context[key] = { values, ...extra }; + }); + } + onAction?.(action.event.name, context); + }; + const handleNext = async () => { try { const fieldsToValidate = currentStep === 0 ? ['username', 'email'] : ['password', 'confirmPassword']; await form.validateFields(fieldsToValidate); const values = form.getFieldsValue(); - if (action?.event?.name) { - const context: Record<string, any> = {}; - if (action.event.context) { - Object.keys(action.event.context).forEach((key) => { - context[key] = { - step: currentStep + 1, - values, - }; - }); - } - onAction?.(action.event.name, context); - } - if (currentStep === 1) { - handleSubmit(values); + emitAction(values, { step: 2, submit: true }); } else { + emitAction(values, { step: currentStep + 1 }); setCurrentStep(currentStep + 1); } } catch (error) { console.log('Validation failed:', error); } }; - - const handleSubmit = (values: any) => { - if (action?.event?.name) { - const context: Record<string, any> = {}; - if (action.event.context) { - Object.keys(action.event.context).forEach((key) => { - context[key] = { - step: 2, - values, - submit: true, - }; - }); - } - onAction?.(action.event.name, context); - } - };Also applies to: 118-131
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx` around lines 83 - 107, handleNext() currently dispatches action.event.name unconditionally and then calls handleSubmit(values) when currentStep === 1, causing the same submit_step event to be sent twice; move the onAction dispatch into the step branches so that for currentStep === 1 you only call handleSubmit (which should dispatch the submit event with submit: true) and for other steps you call onAction as the non-submit intermediate event. Update the logic in handleNext (and the duplicate logic at lines 118-131) to build and send context only in the branch that dispatches the event, referencing handleNext, handleSubmit, onAction, currentStep, setCurrentStep, and action.event to locate and change the code.packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx-527-532 (1)
527-532:⚠️ Potential issue | 🟠 Major流式文案结束后会被自己过滤掉。
这里把文本节点的
timestamp只在RUNNING时设值,状态变成FINISHED后就写成0,随后又被.filter()删除,所以欢迎文案在流结束后会直接消失。useStreamText()已经返回了稳定时间戳,直接复用它即可。🛠️ 一个直接的改法
const { text: textHeader, streamStatus: streamStatusHeader, + timestamp: textHeaderTimestamp, run: runHeader, reset: resetHeader, } = useStreamText(contentHeader); @@ content: { - texts: [ - { text: textHeader, timestamp: streamStatusHeader === 'RUNNING' ? Date.now() : 0 }, - ].filter((item) => item.timestamp !== 0), + texts: + textHeader.length > 0 + ? [{ text: textHeader, timestamp: textHeaderTimestamp || Date.now() }] + : [], card, } as ContentType,Also applies to: 559-565
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/form-validation.tsx` around lines 527 - 532, The streamed welcome message disappears because code sets text node timestamps to 0 when streamStatusHeader becomes FINISHED and then filters out zero-timestamp entries; instead reuse the stable timestamp returned by useStreamText so entries keep their timestamp after FINISHED. Locate the variables from useStreamText (textHeader, streamStatusHeader, runHeader, resetHeader) and stop resetting timestamp to 0 on FINISHED — assign the stable timestamp that useStreamText provides (or preserve the existing timestamp) before any .filter() so the welcome text remains after streaming ends.packages/x-card/src/A2UI/format/components.ts-37-40 (1)
37-40:⚠️ Potential issue | 🟠 Majorv0.8 这里不要提前解包
literalString。
packages/x-card/src/A2UI/Card.v0.8.ts的resolvePropsV08()还依赖{ literalString: string }和路径字符串的区分。这里先把它拍平成普通字符串后,像{ literalString: '/user/name' }这样的合法字面量会在后续再次被当成 path 解析,结果把展示文本误解析成数据绑定。🛠️ 一个直接的改法
if (isPathObject(val)) { props[key] = val.path; } else if (isLiteralStringValue(val)) { - props[key] = val.literalString; + props[key] = val; } else { props[key] = val; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/format/components.ts` around lines 37 - 40, The code prematurely unwraps literalString in components.ts (the branch using isLiteralStringValue) which breaks resolvePropsV08's need to distinguish { literalString: string } objects from plain path strings; change the assignment in that branch to preserve the original literal-string object (i.e., assign props[key] = val or the same wrapper object) instead of props[key] = val.literalString so resolvePropsV08 can detect literal vs path correctly; update any nearby logic that assumes a raw string only if necessary and keep references to isLiteralStringValue and resolvePropsV08 for locating the change.packages/x-card/src/A2UI/Card.v0.9.ts-29-35 (1)
29-35:⚠️ Potential issue | 🟠 Major递归解析没有覆盖嵌套
action/event。这里仅对顶层
action做了专门处理。只要action出现在数组项或子对象里,通用递归就会把event.context里的{ path }提前解引用,后面的写回逻辑就拿不到目标路径了。修复示例
if (val && typeof val === 'object') { const result: Record<string, any> = {}; for (const [k, v] of Object.entries(val)) { - result[k] = resolveValueV09(v, dataModel); + if (k === 'action') { + result[k] = resolveActionPropV09(v, dataModel); + } else if (k === 'event') { + result[k] = resolveActionEventV09(v, dataModel); + } else { + result[k] = resolveValueV09(v, dataModel); + } } return result; }Also applies to: 104-108
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/Card.v0.9.ts` around lines 29 - 35, The recursion only special-cases top-level "action" in the for...of loop, so nested action objects inside arrays or child objects get processed by resolveValueV09 and thus prematurely dereference event.context.path; fix by updating the recursive resolver(s) to detect and specially-handle any encountered property named "action" (both when iterating object entries and when mapping arrays) and call resolveActionPropV09 for that value instead of resolveValueV09; specifically change the object/array traversal logic used by resolveValueV09 (and the for...of loop you already modified) to branch on key === 'action' and delegate to resolveActionPropV09 so nested action/event.context paths remain intact (also apply the same change for the other occurrence around the 104-108 block).
🟡 Minor comments (7)
packages/x-card/src/A2UI/__tests__/catalog.test.ts-53-65 (1)
53-65:⚠️ Potential issue | 🟡 Minor这个测试现在不会真正验证失败分支。
Line 63-65 只是吞掉 rejected promise;如果
loadCatalog()意外 resolve,这个用例也会通过。这里最好显式断言 reject,并顺带校验fetch确实被调用了。💡 建议修改
- it('should not register catalog without id', () => { + it('should not register catalog without id', async () => { const catalog: Catalog = { components: { Button: { type: 'object' } }, }; registerCatalog(catalog); // 没有 id,不应该被注册,fetch 会被调用 mockFetch.mockResolvedValueOnce({ ok: false, statusText: 'Not Found', }); - return loadCatalog('undefined-catalog').catch(() => { - // 预期会失败 - }); + await expect(loadCatalog('undefined-catalog')).rejects.toThrow(); + expect(mockFetch).toHaveBeenCalledWith('undefined-catalog'); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/__tests__/catalog.test.ts` around lines 53 - 65, The test currently swallows a rejected promise and can falsely pass if loadCatalog unexpectedly resolves; change the test to explicitly assert the rejection of loadCatalog('undefined-catalog') and verify mockFetch was called: call registerCatalog(...) as before, then either return expect(loadCatalog('undefined-catalog')).rejects.toBeTruthy() and afterwards assert mockFetch was called, or use async/await with try/catch where a resolved path calls fail(), the catch asserts the error and then checks mockFetch.mockResolvedValueOnce was used (i.e. mockFetch was called); locate this logic around registerCatalog and loadCatalog in the test to update it.packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx-142-144 (1)
142-144:⚠️ Potential issue | 🟡 Minor
Space组件vertical属性使用错误与 v0.8 demo 相同的问题,Ant Design
Space组件应使用direction="vertical"而非vertical布尔属性。🔧 建议修复
- <Space vertical style={{ width: '100%' }} size={12}> + <Space direction="vertical" style={{ width: '100%' }} size={12}> {children} </Space>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 142 - 144, The Space component is using a non-existent boolean prop vertical; update the Space usage in the demo (the Space element wrapping {children}) to use direction="vertical" instead of vertical and keep other props (style, size) unchanged so it renders vertically with Ant Design v0.9.packages/x/docs/x-card/demo/A2UI_v0.8/nested-interaction.tsx-542-585 (1)
542-585:⚠️ Potential issue | 🟡 Minor
useStreamTexthook 缺少组件卸载时的定时器清理当组件卸载时,如果定时器仍在运行,会导致内存泄漏和对已卸载组件的状态更新警告。建议添加 cleanup effect。
🔧 建议添加清理逻辑
const useStreamText = (text: string) => { const textRef = React.useRef(0); const [textIndex, setTextIndex] = React.useState(0); const textTimestamp = React.useRef(0); const [streamStatus, setStreamStatus] = useState('INIT'); const timerRef = useRef<NodeJS.Timeout | null>(null); + // Cleanup on unmount + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + const run = useCallback(() => { // ... existing code }, [text]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/nested-interaction.tsx` around lines 542 - 585, Add a cleanup effect to useStreamText that clears the interval stored in timerRef when the component using the hook unmounts: implement a React.useEffect with an empty dependency array that returns a cleanup function which checks timerRef.current, calls clearInterval(timerRef.current) and sets timerRef.current = null; this ensures any interval started by run is cleared on unmount (complementing the existing reset logic) and prevents state updates after unmount.packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx-567-615 (1)
567-615:⚠️ Potential issue | 🟡 Minor
useStreamTexthook 缺少卸载清理与 v0.8 demo 中的
useStreamText存在相同问题,缺少组件卸载时的定时器清理。建议添加 cleanup effect。🔧 建议添加清理逻辑
const useStreamText = (text: string) => { // ... existing refs and state + useEffect(() => { + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, []); + const run = useCallback(() => { // ... }, [text]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 567 - 615, The useStreamText hook leaks timers because timerRef is not cleared on component unmount; add a cleanup effect inside useStreamText that clears and nulls timerRef (clearInterval(timerRef.current) and timerRef.current = null) to run on unmount (useEffect(() => () => { ... }, [])), ensuring any running interval started by run() is cancelled when the component using useStreamText unmounts; update references to timerRef, run, reset, and ensure streamStatus/textIndex state remain consistent after cleanup.packages/x-card/src/A2UI/utils.ts-13-27 (1)
13-27:⚠️ Potential issue | 🟡 Minor添加路径验证以防止原型污染(可选改进)
setValueByPath函数虽然使用动态路径分段进行对象属性赋值,但实际风险较低,因为路径来自内部配置而非外部用户输入。不过,添加防御性验证是不错的安全实践。建议:过滤危险属性名
+const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype']; + +function isSafeKey(key: string): boolean { + return !UNSAFE_KEYS.includes(key); +} + export function setValueByPath( obj: Record<string, any>, path: string, value: any, ): Record<string, any> { const parts = path.replace(/^\//, '').split('/'); + + if (!parts.every(isSafeKey)) { + console.warn('Unsafe path detected:', path); + return obj; + } + const next = { ...obj }; let cur: Record<string, any> = next; for (let i = 0; i < parts.length - 1; i++) { cur[parts[i]] = cur[parts[i]] ? { ...cur[parts[i]] } : {}; cur = cur[parts[i]]; } cur[parts[parts.length - 1]] = value; return next; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/utils.ts` around lines 13 - 27, setValueByPath currently writes path segments directly into objects which could enable prototype pollution if untrusted paths are ever used; update setValueByPath to validate/filter each segment before assignment (in the function setValueByPath) by rejecting or skipping dangerous keys such as "__proto__", "prototype", "constructor" (and optionally keys starting with "constructor" or "__"), and throw or return the original object when such a segment is encountered; perform this check inside the loop that iterates parts so dangerous segments are never used to create/assign properties and ensure the function still returns a shallow-copied top-level object (next) as before.packages/x/docs/x-card/demo/A2UI_v0.8/streaming.tsx-555-562 (1)
555-562:⚠️ Potential issue | 🟡 Minor会同时出现两条进度条。
RestaurantList已经根据isStreaming渲染了LoadingProgress,这里再挂一个根级progress节点会让加载阶段出现双进度条;而且这个根级节点的status被写死为active,完成后还会一直停在 100%。建议保留一处进度展示即可。🛠️ 一种更直接的收敛方式
- { - id: 'progress', - component: { - LoadingProgress: { - percent: { path: '/progress' }, - status: { literalString: 'active' }, - }, - }, - }, { id: 'restaurant-list', component: { RestaurantList: { @@ Container: { children: { - explicitList: ['title', 'progress', 'restaurant-list'], + explicitList: ['title', 'restaurant-list'], }, }, },Also applies to: 577-579
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/streaming.tsx` around lines 555 - 562, The root-level progress node with id 'progress' is causing a duplicate progress bar alongside RestaurantList's own LoadingProgress (rendered based on isStreaming) and also hardcodes status to 'active' so it stays at 100%; remove the root-level progress node (the entry with id 'progress' and component LoadingProgress) or at minimum stop duplicating by binding its status/percent to the streaming state (e.g., use the same /progress percent and dynamic status rather than literal 'active'); also apply the same removal/fix to the similar block at lines 577-579 so only RestaurantList controls the visible progress indicator.packages/x-card/src/A2UI/Card.v0.9.ts-159-165 (1)
159-165:⚠️ Potential issue | 🟡 Minor不要把
undefined当成“未提供”。这里按
componentValue !== undefined过滤,会让组件显式清空值时更新被吞掉,旧值继续留在dataModel。建议改成按 key 是否存在判断。修复示例
- const componentValue = componentContext[key]; - if (componentValue !== undefined) { - updates.push({ path: (val as { path: string }).path, value: componentValue }); - } + if (Object.prototype.hasOwnProperty.call(componentContext, key)) { + updates.push({ path: (val as { path: string }).path, value: componentContext[key] }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/Card.v0.9.ts` around lines 159 - 165, The loop over Object.entries(context) currently ignores component-provided explicit undefined values because it uses "componentValue !== undefined"; change the presence check to whether the key exists on componentContext (e.g., use Object.prototype.hasOwnProperty.call(componentContext, key) or "key in componentContext") so explicit clears are treated as updates; update the branch that pushes to updates (in the loop referencing context, isPathObject, componentContext, and updates) to use that existence check and still push { path: (val as { path: string }).path, value: componentContext[key] } so dataModel receives explicit null/undefined clears.
🧹 Nitpick comments (13)
packages/x/docs/x-card/demo/A2UI_v0.9/catalog-streaming.json (1)
236-236: 空的 functions 对象当前
functions对象为空。如果这是为未来功能预留的占位符,建议在注释中说明;如果不需要,可以考虑移除以保持文件简洁。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-streaming.json` at line 236, The JSON contains an empty "functions" object which is ambiguous; either remove the "functions" key if it's unused to keep the file concise, or replace it with a documented placeholder (e.g., add a JSON string field like "_note": "functions placeholder for future use") or an adjacent comment in your repo docs explaining it's reserved; locate the "functions" object in the JSON and apply one of these two fixes to make intent explicit.packages/x/docs/react/introduce.en-US.md (1)
17-19: 链接文本可改进(可选)静态分析工具提示 "here" 作为链接文本描述性不足,不利于可访问性。不过当前文件中其他章节也使用相同的 "here" 模式(如第 11、15、23、27 行),建议统一改进或保持一致。
♻️ 建议的改进(可选)
-A dynamic card rendering component based on the A2UI protocol, enabling AI Agents to dynamically build interactive interfaces through structured JSON message streams. Supports streaming rendering, data binding, and reactive updates. Suitable for AI assistant interfaces, smart forms, data visualization, and more. For details, click [here](/x-cards/introduce). +A dynamic card rendering component based on the A2UI protocol, enabling AI Agents to dynamically build interactive interfaces through structured JSON message streams. Supports streaming rendering, data binding, and reactive updates. Suitable for AI assistant interfaces, smart forms, data visualization, and more. For details, see the [X-Card documentation](/x-cards/introduce).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/react/introduce.en-US.md` around lines 17 - 19, Replace the non-descriptive link text "here" in the README snippet for "@ant-design/x-card" (the markdown link [here](/x-cards/introduce)) with a more descriptive label like "Introduction", "Documentation: Introduction", or "x-cards 介绍", and apply the same descriptive wording consistently to the other occurrences of [here] in this file to improve accessibility and maintain consistency across the document.packages/x/docs/x-card/a2ui-v-0-8.en-US.md (1)
27-27: v0.8 文档页面中 commands 类型顺序可能需要调整。在 v0.8 文档页面,
commands属性类型显示为A2UICommand_v0_9 | XAgentCommand_v0_8,建议将 v0.8 类型放在前面以保持一致性:XAgentCommand_v0_8 | A2UICommand_v0_9。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/a2ui-v-0-8.en-US.md` at line 27, The table entry for the commands property currently lists the types as "A2UICommand_v0_9 | XAgentCommand_v0_8"; update that cell so the v0.8 type appears first by changing it to "XAgentCommand_v0_8 | A2UICommand_v0_9" to match v0.8 documentation ordering—locate the commands row in the A2UI docs (the table row containing "commands | A2UI command object") and swap the type order accordingly.packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx (2)
105-108: 生产代码中存在 console.log。建议在 demo 代码中移除或替换
console.log,或者至少添加注释说明这是用于调试目的。♻️ 建议的修复
} catch (error) { // Validation failed, do nothing - console.log('Validation failed:', error); + // Validation failed - form displays error messages to user }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx` around lines 105 - 108, The catch block currently uses console.log('Validation failed:', error) which is not appropriate for production/demo code; replace this with either removing the console statement, using a proper logger (e.g., processLogger.error or a project-provided logger), or add a clear comment that it's only for debugging if you must keep it; update the catch in the form validation code to avoid console.log by using the chosen logger or no-op and preserve the original validation handling.
537-543: useEffect 依赖数组包含 sessionKey 可能导致意外行为。当
sessionKey变化时(重新加载时),此 effect 会立即执行,但此时streamStatusHeader仍为 'INIT',所以不会触发命令。不过,将sessionKey包含在依赖数组中可能会在未来重构时引入 bug。建议添加注释说明此设计意图,或考虑使用 ref 来追踪 session 变化。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.8/form-validation.tsx` around lines 537 - 543, The useEffect watching streamStatusHeader and sessionKey can run unexpectedly when sessionKey changes; update the effect to avoid accidental triggers by either (a) removing sessionKey from the dependency array and documenting the intent with a clear comment above the useEffect referencing streamStatusHeader and onAgentCommand/FormSurfaceUpdateCommand/FormDataModelUpdateCommand/FormBeginRenderingCommand, or (b) keep sessionKey out of deps and instead track session changes with a ref (e.g., sessionKeyRef) and check sessionKeyRef.current inside the effect before calling onAgentCommand, ensuring the commands only fire when streamStatusHeader === 'FINISHED'; include a short inline comment describing the chosen approach to prevent future regressions.packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md (1)
2-2: 文档排序可能冲突。v0.8 和 v0.9 文档页面都设置了
order: 2,这可能导致文档导航中的排序不确定。建议为不同版本设置不同的 order 值(例如 v0.8 为 2,v0.9 为 3)。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md` at line 2, The frontmatter "order" value is duplicated between versions and causes navigation ordering conflicts; update the frontmatter in the v0.9 doc (the "order" key in packages/x/docs/x-card/a2ui-v-0-9.zh-CN.md) to a unique value (e.g., change order from 2 to 3) so it does not collide with the v0.8 page—ensure the "order" key in the v0.8 file remains 2 and the v0.9 file uses the new distinct integer.packages/x-card/tests/setup.ts (1)
20-25: matchMedia mock 使用了已弃用的 listener API。
addListener和removeListener是已弃用的 API,现代浏览器使用addEventListener和removeEventListener。建议同时提供两组方法以确保测试兼容性。♻️ 建议的修复
value: jest.fn((query) => ({ matches: query.includes('max-width'), addListener: jest.fn(), removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), })),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/tests/setup.ts` around lines 20 - 25, The matchMedia mock in tests/setup.ts currently exposes only the deprecated addListener/removeListener methods; update the jest.fn mock returned by the value function (the anonymous function passed to jest.fn) to also provide modern DOM event methods addEventListener and removeEventListener (as jest.fn()), while keeping the existing addListener/removeListener for backward compatibility; ensure the mock shape returned by that function includes matches, addEventListener, removeEventListener, addListener, and removeListener so tests using either API succeed.packages/x/docs/x-card/demo/A2UI_v0.9/catalog-cart.json (1)
359-383:$defs.DynamicValue已定义但未被引用
$defs.DynamicValue定义了通用的动态值模式,但各组件属性中重复定义了相同的oneOf模式而未使用$ref。可以考虑使用引用来减少重复。♻️ 示例:使用 $ref 引用
"text": { "description": "The text content to display.", "$ref": "#/$defs/DynamicValue" }替代当前重复的
oneOf定义。这是可选的优化,当前实现功能正确。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/catalog-cart.json` around lines 359 - 383, The schema defines a reusable pattern as $defs.DynamicValue but other component properties duplicate the same oneOf block instead of referencing it; replace those repeated oneOf definitions with a JSON Schema $ref to "#/$defs/DynamicValue" (e.g., update property schemas that currently inline the oneOf to use "$ref": "#/$defs/DynamicValue") so the DynamicValue definition is actually reused and duplication is eliminated.packages/x-card/src/A2UI/__tests__/integration.test.tsx (2)
573-630:process.env.NODE_ENV修改后的恢复应使用try/finally如果测试在恢复环境变量之前抛出异常,
NODE_ENV将保持修改状态,可能影响后续测试。建议使用try/finally确保恢复。♻️ 建议改进
it('should validate component against catalog in development', async () => { const originalEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; + try { registerCatalog({ // ... catalog definition }); render(/* ... */); await waitFor(() => { expect(console.warn).toHaveBeenCalled(); }); - - process.env.NODE_ENV = originalEnv; + } finally { + process.env.NODE_ENV = originalEnv; + } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/__tests__/integration.test.tsx` around lines 573 - 630, The test "should validate component against catalog in development" mutates process.env.NODE_ENV but doesn't guarantee restoration on failure; wrap the part that sets process.env.NODE_ENV = 'development' and the subsequent registerCatalog/render/assert logic in a try/finally so that process.env.NODE_ENV is always reset to originalEnv in the finally block (keep the existing registerCatalog, render(Box ...), waitFor and console.warn assertions intact and only move the restoration into finally).
709-713:waitFor中使用无意义的断言
expect(true).toBe(true)不会等待任何实际条件,waitFor会立即通过。如果需要等待 effect 执行,应该使用更有意义的断言或考虑使用act配合await。♻️ 建议改进
// 等待 catalog 加载完成(使用 setTimeout 让 useEffect 执行完成) await waitFor(() => { - // 由于 catalog 已通过 registerCatalog 注册,loadCatalog 会直接返回缓存的 catalog - // 不需要检查 console.log,只需等待足够时间让 effect 执行 - expect(true).toBe(true); + // 由于 catalog 已通过 registerCatalog 注册,直接可用 + // 可以检查 Card 是否已渲染 + expect(screen.getByTestId('card1') || true).toBeTruthy(); });或者如果只是需要让 React 完成更新,可以简化为:
await act(async () => { await Promise.resolve(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/__tests__/integration.test.tsx` around lines 709 - 713, The test uses waitFor with a no-op assertion (expect(true).toBe(true)) which makes waitFor succeed immediately; update the test in packages/x-card/src/A2UI/__tests__/integration.test.tsx to wait for a real side-effect or state change instead (e.g., assert the catalog is cached/registered or that a DOM node updated) by replacing the dummy assertion inside the waitFor callback that references the catalog registration with a concrete expect against the relevant value, or alternatively replace the waitFor block with an explicit React update flush using act (await act(async () => { await Promise.resolve(); })) to ensure effects in the component under test have run; locate usages of waitFor and the surrounding comments about registerCatalog to apply the change.packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx (1)
845-852: useEffect 依赖项说明此 useEffect 使用了
onAgentCommand函数,但未将其添加到依赖数组。由于onAgentCommand在组件内定义且引用 setter 函数,这里不会导致问题,但如果使用了 exhaustive-deps lint 规则可能会警告。考虑将
onAgentCommand用useCallback包装或添加禁用 lint 注释。🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x/docs/x-card/demo/A2UI_v0.9/basic.tsx` around lines 845 - 852, The useEffect depends on the onAgentCommand function but doesn't include it in the dependency array, which can trigger exhaustive-deps warnings; wrap onAgentCommand in useCallback (preserving its current setter-based logic) so it has a stable identity, then include onAgentCommand in the useEffect deps, or alternatively explicitly document and disable the lint rule for that effect; reference the onAgentCommand definition and the useEffect block that checks streamStatusHeader === 'FINISHED' to apply the change.packages/x-card/src/A2UI/__tests__/utils.test.ts (1)
60-110: 建议添加原型污染场景的测试用例鉴于
setValueByPath存在原型污染风险(如utils.ts中所述),建议添加测试用例验证函数对恶意路径的处理行为。🧪 建议添加的测试用例
it('should handle __proto__ path safely', () => { const obj = { name: 'Alice' }; const result = setValueByPath(obj, '/__proto__/polluted', 'yes'); // 验证 Object.prototype 未被污染 expect(({} as any).polluted).toBeUndefined(); }); it('should handle constructor path safely', () => { const obj = { name: 'Alice' }; const result = setValueByPath(obj, '/constructor/prototype/polluted', 'yes'); expect(({} as any).polluted).toBeUndefined(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/__tests__/utils.test.ts` around lines 60 - 110, Add tests for prototype-pollution paths to ensure setValueByPath (in utils.ts) does not modify Object.prototype or other constructors: add cases calling setValueByPath(obj, '/__proto__/polluted', 'yes') and setValueByPath(obj, '/constructor/prototype/polluted', 'yes') and assert that ({} as any).polluted is undefined after each call; if tests fail, update setValueByPath to skip keys like "__proto__", "constructor", "prototype" when traversing/creating path segments to prevent prototype pollution.packages/x-card/src/A2UI/types/command_v0.9.ts (1)
46-49: 如果运行时允许无 catalog 的 surface,这里的类型别把catalogId写成必填。同一组变更里已经有 falsy
catalogId的处理分支,现在这个公开类型仍然要求调用方传字符串,只会把使用者逼到传''这种占位值。建议让类型和运行时语义二选一地收敛:要么把字段改成可选,要么删掉运行时分支并在入口校验。🛠️ 若该场景是合法输入,可直接这样收敛
interface CreateSurfaceCommand { version: 'v0.9'; createSurface: { surfaceId: string; - catalogId: string; // 必需,组件目录 URL 或本地标识 + catalogId?: string; // 可选;未提供时跳过 catalog 加载 }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/x-card/src/A2UI/types/command_v0.9.ts` around lines 46 - 49, The public type for createSurface currently forces catalogId to be a required string while runtime accepts falsy values—change the type to reflect runtime by making catalogId optional (use catalogId?: string) in the createSurface definition in command_v0.9.ts and update any related exported interfaces or consumers that assume it is non-null so they handle undefined/falsy cases; alternatively, if you prefer strictness, remove the runtime branch that accepts falsy catalogId and add validation at the entry point, but do not leave the public type out of sync with runtime behavior.
| antd: '^6.1.1', | ||
| '@ant-design/x': '^2.0.0', | ||
| '@ant-design/x-markdown': '^2.0.0', | ||
| '@ant-design/x-card': '^2.0.0', |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
npm view `@ant-design/x-card` version versions --jsonRepository: ant-design/x
Length of output: 775
@ant-design/x-card 在 npm 公开注册表中不存在。
Line 65 引用的依赖 @ant-design/x-card@^2.0.0 在 npm 注册表中返回 404 错误。由于 Sandpack 是浏览器端代码沙箱环境,它需要从 npm 注册表解析并安装依赖,因此无法使用不存在于公开注册表中的包。这会导致示例完全无法加载依赖。
需确认:
- 该包是否尚未发布到 npm
- 包名是否正确
- 如果包是私有或仅供本地开发,需采用其他方式(如 workspaces)来配置 Sandpack 依赖
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/x/.dumi/theme/builtins/Sandpack/index.tsx` at line 65, The
dependency list in the Sandpack builtin includes a nonexistent package entry
'@ant-design/x-card': '^2.0.0' which causes npm 404s; locate the dependencies
object in packages/x/.dumi/theme/builtins/Sandpack/index.tsx and either (a)
correct the package name to the published package, (b) remove the entry if the
component should not be bundled, or (c) if this is a local/private package,
replace the npm entry with a Sandpack-compatible resolution such as pointing to
the package's published CDN build or configuring Sandpack to use your
workspace/local build (e.g., aliasing to the local package bundle); update the
dependencies mapping and any related import/alias logic so Sandpack can resolve
the module at runtime.
Bundle ReportChanges will increase total bundle size by 92 bytes (0.0%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: antdx-array-pushAssets Changed:
|

中文版模板 / Chinese template
🤔 This is a ...
🔗 Related Issues
💡 Background and Solution
📝 Change Log
Summary by CodeRabbit
发布说明
新功能
@ant-design/x-card动态卡片渲染器,支持基于 A2UI 协议构建交互式界面文档