\{copy\.label\}<\/strong>/);
});
@@ -95,50 +176,100 @@ test("does not reserve delete-button space on the non-removable root agent", ()
assert.match(source, /actions && data\.path !== undefined && data\.path\.length > 0/);
});
-test("keeps boundary insertion controls inside the group container", () => {
- assert.match(source, /type !== "parallel" &&/);
- assert.match(source, /className="abc-group-boundary-actions"/);
- assert.match(source, /className="abc-group-boundary-add is-start nodrag nopan"/);
- assert.match(source, /aria-label="添加到最前"/);
- assert.match(source, /actions\.onInsert\(data\.path!, 0\)/);
- assert.match(source, /className="abc-group-boundary-add is-end nodrag nopan"/);
- assert.match(source, /aria-label="添加到最后"/);
- assert.match(source, /className="abc-group-add abc-group-add-empty nodrag nopan"/);
- assert.match(cssSource, /\.abc-group-boundary-actions\s*\{/);
- assert.match(cssSource, /\.abc-group-boundary-add\s*\{/);
- assert.match(cssSource, /\.abc-group-add-empty,\n\.abc-group-add-bottom\s*\{/);
+test("uses the dashed add action without duplicate group boundary buttons", () => {
+ assert.match(groupSource, /className="abc-group-summary-add nodrag nopan"/);
+ assert.doesNotMatch(groupSource, /abc-group-boundary-actions/);
+ assert.doesNotMatch(groupSource, /aria-label="添加到最前"|aria-label="添加到最后"/);
+ assert.doesNotMatch(cssSource, /\.abc-group-boundary-add/);
});
-test("uses a single bottom add action for parallel groups", () => {
- assert.match(source, /type === "parallel" &&/);
- assert.match(source, /className="abc-group-add abc-group-add-bottom nodrag nopan"/);
- assert.match(source, /GROUP_ADD_GAP \+ GROUP_ADD_HEIGHT/);
- assert.match(source, /sizes\.length && type !== "parallel"/);
- assert.match(cssSource, /\.abc-group-add-empty,\n\.abc-group-add-bottom\s*\{/);
+test("keeps the sub-Agent action inside every editable Agent card", () => {
+ assert.match(source, /className="abc-agent-card-add nodrag nopan"/);
+ assert.match(source, /\{data\.description\}<\/p>[\s\S]*?abc-agent-card-add/);
+ assert.match(source, /onAdd=\{[\s\S]*?actions && data\.path !== undefined/);
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-add\s*\{[\s\S]*?width:\s*100% !important;[\s\S]*?height:\s*26px !important;[\s\S]*?justify-content:\s*center !important;[\s\S]*?padding:\s*4px 6px !important;[\s\S]*?border:\s*1px dashed #d8dddb !important;[\s\S]*?font-size:\s*11px !important;[\s\S]*?font-weight:\s*400 !important;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-add::before\s*\{[\s\S]*?box-shadow:\s*none !important;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-add:hover\s*\{[\s\S]*?border-color:\s*var\(--abc-agent-card-tone\) !important;[\s\S]*?background:\s*var\(--abc-agent-card-tone\) !important;/,
+ );
+ assert.match(groupSource, /className="abc-group-summary-add nodrag nopan"/);
+ assert.match(groupSource, /actions && data\.path !== undefined/);
});
-test("uses the agent description as a two-line subtitle without count badges", () => {
+test("matches the Figma node shell and shows description plus capability counts", () => {
+ assert.match(
+ source,
+ /description: agent\.description\.trim\(\) \|\| "描述未配置"/,
+ );
+ assert.doesNotMatch(source, /instruction: agent\.instruction/);
assert.match(
source,
- /description: agent\.description\.trim\(\) \|\| PATTERN_COPY\[type\]\.description/,
+ /agent\.modelName\?\.trim\(\) \|\| agent\.model\?\.trim\(\) \|\| "模型未配置"/,
+ );
+ assert.match(source, /AgentToolCountIcon/);
+ assert.match(source, /AgentSkillCountIcon/);
+ assert.match(source, /className="abc-agent-card-identity"/);
+ assert.match(source, /toolCount: uniqueCapabilityCount/);
+ assert.match(source, /skillCount: uniqueCapabilityCount/);
+ assert.match(
+ cssSource,
+ /\.abc-agent-card\s*\{[\s\S]*?--abc-agent-card-tone:\s*#e8ebf9;[\s\S]*?box-sizing:\s*border-box;[\s\S]*?width:\s*214px;[\s\S]*?height:\s*100%;[\s\S]*?padding:\s*8px;[\s\S]*?border:\s*0\.5px solid var\(--abc-agent-card-tone\);[\s\S]*?border-radius:\s*12px;[\s\S]*?background:\s*#fff;[\s\S]*?box-shadow:\s*none;[\s\S]*?text-align:\s*left;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-head\s*\{[\s\S]*?flex:\s*0 0 42px;[\s\S]*?border-radius:\s*6px;[\s\S]*?background:\s*var\(--abc-agent-card-tone\);/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-identity\s*\{[\s\S]*?gap:\s*3px;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-head strong\s*\{[\s\S]*?font-size:\s*12px;[\s\S]*?line-height:\s*16px;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-main p\s*\{[\s\S]*?font-size:\s*10\.5px;[\s\S]*?line-height:\s*15px;[\s\S]*?-webkit-line-clamp:\s*2;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-stats\s*\{[\s\S]*?gap:\s*6px;[\s\S]*?border-top:\s*1px solid #ececef;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-stats > span\s*\{[\s\S]*?border-radius:\s*6px;[\s\S]*?background:\s*#f5f5f5;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-agent-card-main\s*\{[\s\S]*?flex:\s*0 0 80px;[\s\S]*?gap:\s*8px;[\s\S]*?padding:\s*8px 0;/,
);
- assert.match(source, /\{data\.description\}<\/small>/);
- assert.doesNotMatch(source, /data\.childCount && /);
- assert.doesNotMatch(source, /个\{childUnit\}|个步骤<\/small>/);
- assert.doesNotMatch(source, /\{childCount\}/);
assert.match(
cssSource,
- /\.abc-group-head small\s*\{[\s\S]*?display: -webkit-box;[\s\S]*?-webkit-line-clamp: 2;/,
+ /\.abc-agent-card-stats\s*\{[\s\S]*?flex:\s*0 0 29px;[\s\S]*?padding:\s*8px 0 0;[\s\S]*?border-top:\s*1px solid #ececef;/,
);
assert.match(
cssSource,
- /\.abc-node-copy > small\s*\{[\s\S]*?-webkit-line-clamp: 2;/,
+ /\.abc-node\.is-selected > \.abc-agent-card\s*\{[\s\S]*?border-color:\s*var\(--abc-agent-card-tone\);[\s\S]*?outline:\s*0;[\s\S]*?box-shadow:\s*inset 0 0 0 0\.5px var\(--abc-agent-card-tone\);/,
+ );
+ assert.doesNotMatch(
+ cssSource,
+ /\.abc-(?:node|group)\.is-selected[^}]*outline-offset/,
);
});
-test("uses distinct restrained type colors and removes the LLM node icon", () => {
+test("uses distinct canvas marks while preserving the Agent type controls", () => {
assert.match(source, /loop:\s*\{[\s\S]*?label:\s*"循环执行"/);
- assert.match(source, /\{type !== "llm" && \(/);
+ assert.match(source, //);
+ assert.match(capabilityIconsSource, /type === "sequential"/);
+ assert.match(capabilityIconsSource, /type === "parallel"/);
+ assert.match(capabilityIconsSource, /type === "loop"/);
assert.match(customCreateSource, //);
assert.match(
customCreateSource,
@@ -148,6 +279,17 @@ test("uses distinct restrained type colors and removes the LLM node icon", () =>
assert.match(customCreateSource, /data-agent-type=\{t\.id\}/);
});
+test("shows model and capability counts only for LLM Agents", () => {
+ assert.match(source, /const COMPACT_NODE_HEIGHT = 138;/);
+ assert.match(source, /const nodeHeight = \(type: AgentType, readOnly: boolean\): number/);
+ assert.match(source, /showsModelCapabilities\(type\)[\s\S]*?NODE_HEIGHT[\s\S]*?COMPACT_NODE_HEIGHT/);
+ assert.match(source, /\{showModelCapabilities && \([\s\S]*?abc-agent-card-model/);
+ assert.match(source, /\{showModelCapabilities && \([\s\S]*?abc-agent-card-stats/);
+ assert.match(groupSource, /\{showModelCapabilities && \([\s\S]*?/);
+ assert.match(groupSource, /\{showModelCapabilities && \([\s\S]*?abc-group-summary-stats/);
+ assert.match(cssSource, /\.abc-node:not\(\.is-llm\)\s*\{[\s\S]*?height:\s*138px;/);
+});
+
test("supports a read-only preview without mutation affordances", () => {
assert.match(source, /readOnly\?: boolean/);
assert.match(source, /interactivePreview\?: boolean/);
@@ -161,75 +303,350 @@ test("supports a read-only preview without mutation affordances", () => {
assert.match(source, /panOnDrag=\{!readOnly \|\| interactivePreview\}/);
});
-test("collapses empty agent groups to their information header in read-only previews", () => {
+test("compacts read-only nodes after mutation actions are removed", () => {
+ assert.match(source, /const READ_ONLY_NODE_HEIGHT = 133;/);
+ assert.match(source, /const READ_ONLY_COMPACT_NODE_HEIGHT = 104;/);
+ assert.match(source, /const READ_ONLY_GROUP_SUMMARY_HEIGHT = 84;/);
+ assert.match(source, /const READ_ONLY_GROUP_COMPACT_SUMMARY_HEIGHT = 46;/);
+ assert.match(source, /const nodeHeight = \(type: AgentType, readOnly: boolean\): number/);
+ assert.match(source, /const groupSummaryHeight = \(type: AgentType, readOnly: boolean\): number/);
+ assert.match(source, /function measureAgent\([\s\S]*?readOnly = false,[\s\S]*?nodeHeight\(type, readOnly\)/);
+ assert.match(source, /measureAgent\(child, \[\.\.\.path, index\], direction, readOnly\)/);
+ assert.match(source, /function buildCanvasGraph\([\s\S]*?readOnly = false,/);
+ assert.match(source, /buildCanvasGraph\(draft, direction, readOnly\)/);
+ assert.doesNotMatch(source, /nodeHeight\(type\);/);
+ assert.match(
+ cssSource,
+ /\.abc-root\.is-readonly \.abc-group\.is-llm\s*\{[\s\S]*?--abc-group-summary-height:\s*84px;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-root\.is-readonly \.abc-group:not\(\.is-llm\)\s*\{[\s\S]*?--abc-group-summary-height:\s*46px;/,
+ );
+ assert.match(
+ cssSource,
+ /\.abc-root\.is-readonly \.abc-agent-card-main\s*\{[\s\S]*?flex:\s*0 0 46px;/,
+ );
+ assert.match(cssSource, /\.abc-root\.is-readonly \.abc-node\.is-llm\s*\{[\s\S]*?height:\s*133px;/);
+ assert.match(cssSource, /\.abc-root\.is-readonly \.abc-node:not\(\.is-llm\)\s*\{[\s\S]*?height:\s*104px;/);
+});
+
+test("keeps the parent identity compact while restoring its full summary", () => {
assert.match(
source,
- /compactEmptyGroups && agent\.subAgents\.length === 0[\s\S]*?height: GROUP_HEADER_HEIGHT/,
+ /const GROUP_HEADER_HEIGHT = 46;/,
+ );
+ assert.match(source, /const GROUP_SUMMARY_HEIGHT = 122;/);
+ assert.match(
+ source,
+ /const GROUP_COMPACT_SUMMARY_HEIGHT = 84;/,
+ );
+ assert.match(source, /const showsModelCapabilities = \(type: AgentType\): boolean => type === "llm";/);
+ assert.match(source, /const groupContentTop = \(type: AgentType, readOnly: boolean\): number/);
+ assert.match(source, /childCount > 0 \? " has-children" : " is-empty"/);
+ assert.match(groupSource, /className="abc-group-summary"/);
+ assert.match(
+ groupSource,
+ /className="abc-group-description" title=\{data\.description\}>[\s\S]*?\{data\.description\}/,
+ );
+ assert.match(groupSource, /className="abc-group-summary-add nodrag nopan"/);
+ assert.match(groupSource, /className="abc-group-summary-stats"/);
+ assert.match(groupSource, /\{showModelCapabilities && \(/);
+ assert.match(groupSource, /[\s\S]*?data\.skillCount/);
+ assert.match(groupSource, /[\s\S]*?data\.toolCount/);
+ assert.doesNotMatch(groupSource, /\{childCount\}/);
assert.match(
cssSource,
- /\.abc-group\.is-compact-empty \.abc-group-head\s*\{[\s\S]*?border-bottom:\s*0;/,
+ /\.abc-group-summary\s*\{[\s\S]*?height:\s*var\(--abc-group-summary-height\);[\s\S]*?padding:\s*8px 16px;[\s\S]*?text-align:\s*left;/,
);
+ assert.match(cssSource, /\.abc-group\s*\{[\s\S]*?--abc-group-summary-height:\s*84px;/);
+ assert.match(cssSource, /\.abc-group\.is-llm\s*\{[\s\S]*?--abc-group-summary-height:\s*122px;/);
+ assert.match(
+ cssSource,
+ /\.abc-group-summary-stats\s*\{[\s\S]*?margin-top:\s*8px;[\s\S]*?padding-top:\s*8px;/,
+ );
+ assert.doesNotMatch(cssSource, /\.abc-group-summary-stats\s*\{[\s\S]*?margin-top:\s*auto;/);
+ assert.match(
+ cssSource,
+ /\.abc-group-description\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/,
+ );
+});
+
+test("measures empty and nested groups below the parent summary", () => {
+ assert.match(
+ source,
+ /if \(sizes\.length === 0\) \{[\s\S]*?width: GROUP_MIN_WIDTH,[\s\S]*?height: contentTop/,
+ );
+ assert.match(
+ source,
+ /height:\s*contentTop \+[\s\S]*?sizes\.reduce/,
+ );
+ assert.match(
+ source,
+ /height:\s*contentTop \+[\s\S]*?tallestChild/,
+ );
+ assert.doesNotMatch(source, /GROUP_HEADER_HEIGHT \+\s*flowPadding/);
+});
+
+test("starts child nodes and parallel rails after the parent summary", () => {
+ assert.match(
+ source,
+ /y:\s*contentTop \+[\s\S]*?type === "parallel" \? PARALLEL_RAIL_SPACE/,
+ );
+ assert.match(source, /y: contentTop \+ cursor/);
+ assert.match(
+ source,
+ /const splitPosition =[\s\S]*?contentTop \+[\s\S]*?const mergePosition =[\s\S]*?contentTop \+/,
+ );
+ assert.doesNotMatch(cssSource, /\.abc-group-boundary-actions/);
});
-test("lays out creation, detail, and deployment previews horizontally", () => {
+test("keeps child Agents inside a visible parent body with semantic flow lines", () => {
+ assert.match(source, /parentId,\s*extent: "parent"/);
+ assert.match(source, /className="abc-group-body"/);
+ assert.match(source, /aria-label=\{`\$\{data\.title\} 的子 Agent 容器`\}/);
+ assert.match(
+ cssSource,
+ /\.abc-group-body\s*\{[\s\S]*?position:\s*absolute;[\s\S]*?border:\s*1px solid var\(--abc-group-tone\);[\s\S]*?background:\s*rgb\(255 255 255 \/ 74%\);/,
+ );
+
+ assert.doesNotMatch(source, /sourceHandle:\s*"child-source"/);
+ assert.doesNotMatch(source, /makeEdge\(id, childId, "调用"/);
+ assert.doesNotMatch(source, /makeEdge\(id, childIds\[0\], "开始"/);
+ assert.match(source, /makeEdge\(childIds\[index\], childIds\[index \+ 1\], "然后"/);
+ assert.match(source, /"继续循环"/);
+
+ assert.match(source, /junctionKind\?:\s*"split" \| "merge"/);
+ assert.match(source, /addParallelJunction\(\s*"split"/);
+ assert.match(source, /addParallelJunction\(\s*"merge"/);
+ assert.doesNotMatch(source, /makeEdge\(id, splitId, "分发"/);
+ assert.match(source, /makeEdge\(splitId, childId/);
+ assert.match(source, /makeEdge\(childId, mergeId/);
+ assert.match(source, /groupExitIds\.set\(id, \[id\]\)/);
+ assert.match(source, /return groupExitIds\.get\(id\) \?\? \[id\]/);
+ assert.match(source, /const topLevelId = \(nodeId: string\)/);
+ assert.match(source, /if \(source !== target\) graph\.setEdge\(source, target\)/);
+ assert.match(source, /function ParallelJunctionNode/);
+ assert.match(cssSource, /\.abc-junction\.is-split \.abc-junction-mark/);
+ assert.match(cssSource, /\.abc-junction\.is-merge \.abc-junction-mark/);
+});
+
+test("selection only strengthens the Agent card border without moving its content", () => {
+ const selectionRule = cssSource.match(
+ /\.abc-node\.is-selected > \.abc-agent-card\s*\{[^}]*\}/,
+ )?.[0] ?? "";
+ assert.match(selectionRule, /box-shadow:\s*inset 0 0 0 0\.5px/);
+ assert.doesNotMatch(selectionRule, /transform|translate|margin|padding/);
+ const groupSelectionRule = cssSource.match(
+ /\.abc-group\.is-selected > \.abc-group-body\s*\{[^}]*\}/,
+ )?.[0] ?? "";
+ assert.match(groupSelectionRule, /inset 0 0 0 0\.5px/);
+ assert.doesNotMatch(groupSelectionRule, /transform|translate|margin|padding/);
+});
+
+test("reuses the session canvas between the Figma chat and configuration panels", () => {
assert.match(source, /direction\?: CanvasDirection/);
+ assert.match(source, /direction = "vertical"/);
assert.match(source, /rankdir: direction === "vertical" \? "TB" : "LR"/);
assert.match(source, /direction === "vertical" \? Position\.Top : Position\.Left/);
assert.match(source, /direction === "vertical" \? Position\.Bottom : Position\.Right/);
- assert.match(customCreateSource, / \.abc-root\s*\{[\s\S]*?flex:\s*0 0 200px;[\s\S]*?width:\s*100%;[\s\S]*?min-height:\s*200px;/,
+ /\.cw-editor > \.abc-root\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;[\s\S]*?border-radius:\s*0;/,
);
assert.match(
customCreateStyles,
- /\.cw-detail\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?width:\s*100%;[\s\S]*?max-width:\s*none;/,
+ /\.cw-detail\s*\{[\s\S]*?flex:\s*0 0 420px;[\s\S]*?width:\s*420px;[\s\S]*?max-width:\s*420px;/,
+ );
+ assert.match(builderChatSource, /