feat(record): implement caching for element descriptions and screenshots to enhance performance

This commit is contained in:
zhouxiao.shaw 2025-06-01 21:28:08 +08:00
parent 4de7fd81f7
commit 091ad4f564
7 changed files with 568 additions and 396 deletions

View File

@ -33,7 +33,7 @@ import {
useRecordStore,
useRecordingSessionStore,
} from '../store';
import { optimizeEvent } from '../utils/eventOptimizer';
import { clearDescriptionCache, optimizeEvent } from '../utils/eventOptimizer';
import './record.less';
// Generate default session name with current time
@ -146,11 +146,11 @@ const safeChromeAPI = {
// Mock port for non-extension environment
return {
onMessage: {
addListener: () => {},
removeListener: () => {},
addListener: () => { },
removeListener: () => { },
},
disconnect: () => {},
postMessage: () => {},
disconnect: () => { },
postMessage: () => { },
};
}
},
@ -234,242 +234,242 @@ const RecordList: React.FC<{
currentTab,
startRecording,
}) => {
return (
<div className="record-list-view">
{!isExtensionMode && (
<Alert
message="Limited Functionality"
description="Recording features require Chrome extension environment. Only session management and event viewing are available."
type="warning"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
return (
<div className="record-list-view">
{!isExtensionMode && (
<Alert
message="Limited Functionality"
description="Recording features require Chrome extension environment. Only session management and event viewing are available."
type="warning"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
<Title level={3} style={{ margin: 0 }}>
Recording Sessions
</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
// Create session with default name and start recording immediately
const sessionName = generateDefaultSessionName();
const newSession: RecordingSession = {
id: `session-${Date.now()}`,
name: sessionName,
createdAt: Date.now(),
updatedAt: Date.now(),
events: [],
status: 'idle',
url: currentTab?.url,
};
addSession(newSession);
setCurrentSession(newSession.id);
clearEvents();
message.success(`Session "${sessionName}" created successfully`);
// Switch to detail view for the new session
setSelectedSession(newSession);
setViewMode('detail');
// Automatically start recording if in extension mode
if (isExtensionMode && currentTab?.id) {
setTimeout(() => {
startRecording();
}, 100);
}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px',
}}
>
New Session
</Button>
</div>
<Title level={3} style={{ margin: 0 }}>
Recording Sessions
</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
// Create session with default name and start recording immediately
const sessionName = generateDefaultSessionName();
const newSession: RecordingSession = {
id: `session-${Date.now()}`,
name: sessionName,
createdAt: Date.now(),
updatedAt: Date.now(),
events: [],
status: 'idle',
url: currentTab?.url,
};
{sessions.length === 0 ? (
<div className="session-empty">
<Empty
description="No recording sessions yet"
style={{ margin: '40px 0' }}
addSession(newSession);
setCurrentSession(newSession.id);
clearEvents();
message.success(`Session "${sessionName}" created successfully`);
// Switch to detail view for the new session
setSelectedSession(newSession);
setViewMode('detail');
// Automatically start recording if in extension mode
if (isExtensionMode && currentTab?.id) {
setTimeout(() => {
startRecording();
}, 100);
}
}}
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
// Create session with default name and start recording immediately
const sessionName = generateDefaultSessionName();
const newSession: RecordingSession = {
id: `session-${Date.now()}`,
name: sessionName,
createdAt: Date.now(),
updatedAt: Date.now(),
events: [],
status: 'idle',
url: currentTab?.url,
};
addSession(newSession);
setCurrentSession(newSession.id);
clearEvents();
message.success(
`Session "${sessionName}" created successfully`,
);
// Switch to detail view for the new session
setSelectedSession(newSession);
setViewMode('detail');
// Automatically start recording if in extension mode
if (isExtensionMode && currentTab?.id) {
setTimeout(() => {
startRecording();
}, 100);
}
}}
>
Create First Session
</Button>
</Empty>
New Session
</Button>
</div>
) : (
<List
className="session-list"
grid={{ gutter: 16, column: 1 }}
dataSource={sessions}
renderItem={(session) => (
<List.Item>
<Card
size="small"
className={
session.id === currentSessionId ? 'selected-session' : ''
}
style={{
cursor: 'pointer',
border:
session.id === currentSessionId
? '2px solid #1890ff'
: '1px solid #d9d9d9',
{sessions.length === 0 ? (
<div className="session-empty">
<Empty
description="No recording sessions yet"
style={{ margin: '40px 0' }}
>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
// Create session with default name and start recording immediately
const sessionName = generateDefaultSessionName();
const newSession: RecordingSession = {
id: `session-${Date.now()}`,
name: sessionName,
createdAt: Date.now(),
updatedAt: Date.now(),
events: [],
status: 'idle',
url: currentTab?.url,
};
addSession(newSession);
setCurrentSession(newSession.id);
clearEvents();
message.success(
`Session "${sessionName}" created successfully`,
);
// Switch to detail view for the new session
setSelectedSession(newSession);
setViewMode('detail');
// Automatically start recording if in extension mode
if (isExtensionMode && currentTab?.id) {
setTimeout(() => {
startRecording();
}, 100);
}
}}
onClick={() => onViewDetail(session)}
actions={[
<Button
key="select"
type="text"
size="small"
onClick={(e) => {
e.stopPropagation();
onSelectSession(session);
}}
style={{
color:
session.id === currentSessionId ? '#1890ff' : undefined,
}}
>
{session.id === currentSessionId ? 'Selected' : 'Select'}
</Button>,
<Button
key="edit"
type="text"
icon={<EditOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onEditSession(session);
}}
/>,
<Button
key="download"
type="text"
icon={<DownloadOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onExportSession(session);
}}
disabled={session.events.length === 0}
/>,
<Popconfirm
key="delete"
title="Delete session"
description="Are you sure you want to delete this session?"
onConfirm={(e) => {
e?.stopPropagation();
onDeleteSession(session.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>,
]}
>
<Card.Meta
title={
<div
Create First Session
</Button>
</Empty>
</div>
) : (
<List
className="session-list"
grid={{ gutter: 16, column: 1 }}
dataSource={sessions}
renderItem={(session) => (
<List.Item>
<Card
size="small"
className={
session.id === currentSessionId ? 'selected-session' : ''
}
style={{
cursor: 'pointer',
border:
session.id === currentSessionId
? '2px solid #1890ff'
: '1px solid #d9d9d9',
}}
onClick={() => onViewDetail(session)}
actions={[
<Button
key="select"
type="text"
size="small"
onClick={(e) => {
e.stopPropagation();
onSelectSession(session);
}}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color:
session.id === currentSessionId ? '#1890ff' : undefined,
}}
>
<span>{session.name}</span>
<Space>
<Tag
color={
session.status === 'recording'
? 'red'
: session.status === 'completed'
? 'green'
: 'default'
}
>
{session.status}
</Tag>
{session.id === currentSessionId && (
<Tag color="blue">Current</Tag>
)}
</Space>
</div>
}
description={
<div className="session-meta">
<div className="session-details">
Events: {session.events.length} | Created:{' '}
{new Date(session.createdAt).toLocaleString()} |
{session.duration &&
` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
{session.url &&
` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
{session.id === currentSessionId ? 'Selected' : 'Select'}
</Button>,
<Button
key="edit"
type="text"
icon={<EditOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onEditSession(session);
}}
/>,
<Button
key="download"
type="text"
icon={<DownloadOutlined />}
size="small"
onClick={(e) => {
e.stopPropagation();
onExportSession(session);
}}
disabled={session.events.length === 0}
/>,
<Popconfirm
key="delete"
title="Delete session"
description="Are you sure you want to delete this session?"
onConfirm={(e) => {
e?.stopPropagation();
onDeleteSession(session.id);
}}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>,
]}
>
<Card.Meta
title={
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span>{session.name}</span>
<Space>
<Tag
color={
session.status === 'recording'
? 'red'
: session.status === 'completed'
? 'green'
: 'default'
}
>
{session.status}
</Tag>
{session.id === currentSessionId && (
<Tag color="blue">Current</Tag>
)}
</Space>
</div>
{session.description && (
<div style={{ marginTop: '4px', fontStyle: 'italic' }}>
{session.description}
}
description={
<div className="session-meta">
<div className="session-details">
Events: {session.events.length} | Created:{' '}
{new Date(session.createdAt).toLocaleString()} |
{session.duration &&
` Duration: ${(session.duration / 1000).toFixed(1)}s |`}
{session.url &&
` URL: ${session.url.slice(0, 50)}${session.url.length > 50 ? '...' : ''}`}
</div>
)}
</div>
}
/>
</Card>
</List.Item>
)}
/>
)}
</div>
);
};
{session.description && (
<div style={{ marginTop: '4px', fontStyle: 'italic' }}>
{session.description}
</div>
)}
</div>
}
/>
</Card>
</List.Item>
)}
/>
)}
</div>
);
};
// Record Detail Component
const RecordDetail: React.FC<{
@ -495,156 +495,156 @@ const RecordDetail: React.FC<{
onExportEvents,
isExtensionMode,
}) => {
return (
<div className="record-detail-view">
{!isExtensionMode && (
<Alert
message="Recording Disabled"
description="Recording functionality is not available outside Chrome extension environment."
type="warning"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
{/* Header with back button and session info */}
<div className="detail-header">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="back-button"
>
Back to Sessions
</Button>
<div className="session-title">
<Title level={4}>{session.name}</Title>
</div>
</div>
{/* Recording Status Indicator */}
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
{isRecording ? (
<span>🔴 Recording in progress</span>
) : (
<span> Ready to record</span>
return (
<div className="record-detail-view">
{!isExtensionMode && (
<Alert
message="Recording Disabled"
description="Recording functionality is not available outside Chrome extension environment."
type="warning"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
</div>
{/* Session Details */}
<Card size="small" className="session-info-card">
<div className="session-info">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div>
<Text strong>Status: </Text>
<Tag
color={
session.status === 'recording'
? 'red'
: session.status === 'completed'
? 'green'
: 'default'
}
{/* Header with back button and session info */}
<div className="detail-header">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="back-button"
>
Back to Sessions
</Button>
<div className="session-title">
<Title level={4}>{session.name}</Title>
</div>
</div>
{/* Recording Status Indicator */}
<div className={`recording-status ${isRecording ? 'recording' : 'idle'}`}>
{isRecording ? (
<span>🔴 Recording in progress</span>
) : (
<span> Ready to record</span>
)}
</div>
{/* Session Details */}
<Card size="small" className="session-info-card">
<div className="session-info">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div>
<Text strong>Status: </Text>
<Tag
color={
session.status === 'recording'
? 'red'
: session.status === 'completed'
? 'green'
: 'default'
}
>
{session.status}
</Tag>
</div>
<div>
<Text strong>Events: </Text>
<Text>{session.events.length}</Text>
</div>
<div>
<Text strong>Created: </Text>
<Text>{new Date(session.createdAt).toLocaleString()}</Text>
</div>
{session.duration && (
<div>
<Text strong>Duration: </Text>
<Text>{(session.duration / 1000).toFixed(1)}s</Text>
</div>
)}
{session.url && (
<div>
<Text strong>URL: </Text>
<Text>{session.url}</Text>
</div>
)}
{session.description && (
<div>
<Text strong>Description: </Text>
<Text>{session.description}</Text>
</div>
)}
</Space>
</div>
</Card>
{/* Recording Controls */}
<div className="controls-section">
<div className="current-tab-info">
<Text strong>Current Tab:</Text>{' '}
{currentTab?.title || 'No tab selected'}
{!isExtensionMode && <Text type="secondary"> (Mock)</Text>}
</div>
<Space className="record-controls">
{!isRecording ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onStartRecording}
disabled={!currentTab || !isExtensionMode}
>
{session.status}
</Tag>
</div>
<div>
<Text strong>Events: </Text>
<Text>{session.events.length}</Text>
</div>
<div>
<Text strong>Created: </Text>
<Text>{new Date(session.createdAt).toLocaleString()}</Text>
</div>
{session.duration && (
<div>
<Text strong>Duration: </Text>
<Text>{(session.duration / 1000).toFixed(1)}s</Text>
</div>
)}
{session.url && (
<div>
<Text strong>URL: </Text>
<Text>{session.url}</Text>
</div>
)}
{session.description && (
<div>
<Text strong>Description: </Text>
<Text>{session.description}</Text>
</div>
Start Recording
</Button>
) : (
<Button
danger
icon={<StopOutlined />}
onClick={onStopRecording}
disabled={!isExtensionMode}
>
Stop Recording
</Button>
)}
<Button
icon={<DeleteOutlined />}
onClick={onClearEvents}
disabled={events.length === 0 || isRecording}
>
Clear Events
</Button>
<Button
icon={<DownloadOutlined />}
onClick={onExportEvents}
disabled={events.length === 0}
>
Export Events
</Button>
</Space>
</div>
</Card>
{/* Recording Controls */}
<div className="controls-section">
<div className="current-tab-info">
<Text strong>Current Tab:</Text>{' '}
{currentTab?.title || 'No tab selected'}
{!isExtensionMode && <Text type="secondary"> (Mock)</Text>}
</div>
<Space className="record-controls">
{!isRecording ? (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={onStartRecording}
disabled={!currentTab || !isExtensionMode}
>
Start Recording
</Button>
) : (
<Button
danger
icon={<StopOutlined />}
onClick={onStopRecording}
disabled={!isExtensionMode}
>
Stop Recording
</Button>
)}
<Divider />
<Button
icon={<DeleteOutlined />}
onClick={onClearEvents}
disabled={events.length === 0 || isRecording}
{/* Events Display */}
<div className="events-section">
<div className="events-header">
<Title level={5}>Recorded Events ({events.length})</Title>
</div>
<div
className={`events-container ${events.length === 0 ? 'empty' : ''}`}
>
Clear Events
</Button>
<Button
icon={<DownloadOutlined />}
onClick={onExportEvents}
disabled={events.length === 0}
>
Export Events
</Button>
</Space>
</div>
<Divider />
{/* Events Display */}
<div className="events-section">
<div className="events-header">
<Title level={5}>Recorded Events ({events.length})</Title>
</div>
<div
className={`events-container ${events.length === 0 ? 'empty' : ''}`}
>
{events.length === 0 ? (
<Empty description="No events recorded yet" />
) : (
<RecordTimeline events={events} />
)}
{events.length === 0 ? (
<Empty description="No events recorded yet" />
) : (
<RecordTimeline events={events} />
)}
</div>
</div>
</div>
</div>
);
};
);
};
export default function Record() {
const {
@ -1036,11 +1036,13 @@ export default function Record() {
}
// Update session status to recording
updateSession(currentSessionId, {
status: 'recording',
url: currentTab?.url,
updatedAt: Date.now(),
});
if (currentSessionId) {
updateSession(currentSessionId, {
status: 'recording',
url: currentTab?.url,
updatedAt: Date.now(),
});
}
if (!currentTab?.id) {
message.error('No active tab found');
@ -1051,6 +1053,9 @@ export default function Record() {
await ensureScriptInjected();
try {
// Clear the AI description cache to avoid using old descriptions
clearDescriptionCache();
// Send message to content script to start recording
await safeChromeAPI.tabs.sendMessage(currentTab.id, { action: 'start' });
setIsRecording(true);

View File

@ -3,6 +3,79 @@ import type { BaseElement, UIContext } from '@midscene/core';
import { compositeElementInfoImg } from '@midscene/shared/img';
import type { RecordedEvent } from '../store';
// Caches for element descriptions and boxed screenshots to improve performance
// Using LRU-like behavior by tracking keys in insertion order and limiting size
const MAX_CACHE_SIZE = 100; // Maximum number of items to keep in each cache
const descriptionCache = new Map<string, string>();
const boxedScreenshotCache = new Map<string, string>();
const cacheKeyOrder: string[] = []; // Track keys in order of insertion for LRU behavior
// Add an item to cache with size limiting
const addToCache = (
cache: Map<string, string>,
key: string,
value: string,
): void => {
// If key already exists, remove it from the order array to add it at the end (most recently used)
const existingIndex = cacheKeyOrder.indexOf(key);
if (existingIndex >= 0) {
cacheKeyOrder.splice(existingIndex, 1);
}
// If cache is at max size, remove oldest item (LRU)
if (cache.size >= MAX_CACHE_SIZE && cacheKeyOrder.length > 0) {
const oldestKey = cacheKeyOrder.shift();
if (oldestKey) {
// Remove from both caches to ensure consistency
descriptionCache.delete(oldestKey);
boxedScreenshotCache.delete(oldestKey);
}
}
// Add new key to cache and track it
cache.set(key, value);
cacheKeyOrder.push(key);
};
// Generate a cache key based on element properties
const generateElementCacheKey = (event: RecordedEvent): string => {
const elementProps = [
event.targetTagName || '',
event.targetId || '',
event.targetClassName || '',
event.viewportX || 0,
event.viewportY || 0,
event.width || 0,
event.height || 0,
];
return elementProps.join('|');
};
// Check if two events reference the same DOM element
const isSameElement = (
event1: RecordedEvent,
event2: RecordedEvent,
): boolean => {
return (
event1.targetId === event2.targetId &&
event1.targetTagName === event2.targetTagName &&
event1.targetClassName === event2.targetClassName &&
Math.abs((event1.viewportX || 0) - (event2.viewportX || 0)) < 5 &&
Math.abs((event1.viewportY || 0) - (event2.viewportY || 0)) < 5 &&
Math.abs((event1.width || 0) - (event2.width || 0)) < 5 &&
Math.abs((event1.height || 0) - (event2.height || 0)) < 5
);
};
// Clear all caches
export const clearDescriptionCache = (): void => {
descriptionCache.clear();
boxedScreenshotCache.clear();
cacheKeyOrder.length = 0; // Clear the key order array
console.log('Description and screenshot caches cleared');
};
// Generate fallback description for events when AI fails
export const generateFallbackDescription = (event: RecordedEvent): string => {
const elementType = event.targetTagName?.toLowerCase() || 'element';
@ -21,7 +94,7 @@ export const generateFallbackDescription = (event: RecordedEvent): string => {
}
};
// Generate AI description asynchronously
// Generate AI description asynchronously with caching
const generateAIDescription = async (
event: RecordedEvent,
boxedImageBase64: string,
@ -29,6 +102,23 @@ const generateAIDescription = async (
updateCallback: (updatedEvent: RecordedEvent) => void,
) => {
try {
// Generate a cache key for this element
const cacheKey = generateElementCacheKey(event);
// Check if we have a cached description for this element
if (descriptionCache.has(cacheKey)) {
const cachedDescription = descriptionCache.get(cacheKey);
console.log('Using cached description for element');
updateCallback({
...eventWithBoxedImage,
elementDescription: cachedDescription,
descriptionLoading: false,
});
return;
}
// No cached description, generate a new one
const mockContext: UIContext<BaseElement> = {
screenshotBase64: boxedImageBase64,
size: { width: event.pageWidth, height: event.pageHeight },
@ -39,6 +129,9 @@ const generateAIDescription = async (
const insight = new Insight(mockContext);
const { description } = await insight.describe([event.x!, event.y!]);
// Cache the description for future use
addToCache(descriptionCache, cacheKey, description);
updateCallback({
...eventWithBoxedImage,
elementDescription: description,
@ -94,31 +187,50 @@ export const optimizeEvent = async (
} as any);
}
// Generate the boxed image
const boxedImageBase64 = await compositeElementInfoImg({
inputImgBase64: event.screenshotBefore,
size: { width: event.pageWidth, height: event.pageHeight },
elementsPositionInfo,
borderThickness: 3,
annotationPadding: 2,
});
// Check for cached description and boxed screenshot by element properties
const cacheKey = generateElementCacheKey(event);
const cachedDescription = descriptionCache.get(cacheKey);
let boxedImageBase64;
// Return event with boxed image and loading state
// Check if we have a cached boxed screenshot
if (boxedScreenshotCache.has(cacheKey)) {
boxedImageBase64 = boxedScreenshotCache.get(cacheKey);
console.log('Using cached boxed screenshot for element');
} else {
// Generate the boxed image and cache it
boxedImageBase64 = await compositeElementInfoImg({
inputImgBase64: event.screenshotBefore,
size: { width: event.pageWidth, height: event.pageHeight },
elementsPositionInfo,
borderThickness: 3,
annotationPadding: 2,
});
// Only cache the boxed image if it's for a significant element (with dimensions)
if (event.width && event.height && event.width > 0 && event.height > 0) {
addToCache(boxedScreenshotCache, cacheKey, boxedImageBase64);
}
}
// Return event with boxed image and loading state or cached description
const eventWithBoxedImage: RecordedEvent = {
...event,
screenshotWithBox: boxedImageBase64,
elementDescription: 'AI 正在分析元素...',
descriptionLoading: true,
elementDescription: cachedDescription || 'AI 正在分析元素...',
descriptionLoading: !cachedDescription,
};
// Generate AI description asynchronously if coordinates are available
// Generate AI description asynchronously if coordinates are available and no cached description
if (event.x !== undefined && event.y !== undefined && updateCallback) {
generateAIDescription(
event,
boxedImageBase64,
eventWithBoxedImage,
updateCallback,
);
// Skip AI description generation if we already have a cached description
if (!cachedDescription && boxedImageBase64) {
generateAIDescription(
event,
boxedImageBase64,
eventWithBoxedImage,
updateCallback,
);
}
} else {
eventWithBoxedImage.elementDescription = 'No description available';
eventWithBoxedImage.descriptionLoading = false;

View File

@ -16,7 +16,7 @@
"dependencies": {
"querystring": "0.2.1",
"react": "18.3.1",
"react-dom": ">=19.1.0"
"react-dom": "18.3.1"
},
"devDependencies": {
"@rspress/plugin-llms": "2.0.0-beta.4",

54
migrations.json Normal file
View File

@ -0,0 +1,54 @@
{
"migrations": [
{
"version": "20.0.0-beta.7",
"description": "Migration for v20.0.0-beta.7",
"implementation": "./src/migrations/update-20-0-0/move-use-daemon-process",
"package": "nx",
"name": "move-use-daemon-process"
},
{
"version": "20.0.1",
"description": "Set `useLegacyCache` to true for migrating workspaces",
"implementation": "./src/migrations/update-20-0-1/use-legacy-cache",
"x-repair-skip": true,
"package": "nx",
"name": "use-legacy-cache"
},
{
"version": "21.0.0-beta.8",
"description": "Removes the legacy cache configuration from nx.json",
"implementation": "./src/migrations/update-21-0-0/remove-legacy-cache",
"package": "nx",
"name": "remove-legacy-cache"
},
{
"version": "21.0.0-beta.8",
"description": "Removes the legacy cache configuration from nx.json",
"implementation": "./src/migrations/update-21-0-0/remove-custom-tasks-runner",
"package": "nx",
"name": "remove-custom-tasks-runner"
},
{
"version": "21.0.0-beta.11",
"description": "Updates release version config based on the breaking changes in Nx v21",
"implementation": "./src/migrations/update-21-0-0/release-version-config-changes",
"package": "nx",
"name": "release-version-config-changes"
},
{
"version": "21.0.0-beta.11",
"description": "Updates release changelog config based on the breaking changes in Nx v21",
"implementation": "./src/migrations/update-21-0-0/release-changelog-config-changes",
"package": "nx",
"name": "release-changelog-config-changes"
},
{
"version": "21.1.0-beta.2",
"description": "Adds **/nx-rules.mdc and **/nx.instructions.md to .gitignore if not present",
"implementation": "./src/migrations/update-21-1-0/add-gitignore-entry",
"package": "nx",
"name": "21-1-0-add-ignore-entries-for-nx-rule-files"
}
]
}

View File

@ -20,5 +20,6 @@
"dependsOn": ["^build"]
}
},
"defaultBase": "main"
"defaultBase": "main",
"nxCloudId": "683c52f96ac2ea619b350be9"
}

View File

@ -53,7 +53,7 @@
"husky": "9.1.7",
"minimist": "1.2.5",
"nano-staged": "^0.8.0",
"nx": "^19.8.14",
"nx": "21.1.2",
"prettier": "^3.5.3",
"pretty-quick": "3.1.3",
"semver": "7.5.2",

View File

@ -25,11 +25,11 @@
"@ant-design/icons": "^5.3.1",
"antd": "^5.21.6",
"dayjs": "^1.11.11",
"react-dom": ">=19.1.0",
"react-dom": "18.3.1",
"@midscene/shared": "workspace:*"
},
"peerDependencies": {
"react": "18.3.1",
"react-dom": ">=19.1.0"
"react-dom": "18.3.1"
}
}