Conversation
🦋 Changeset detectedLatest commit: 4525c03 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
Claude finished @dimaMachina's task —— View job Code Review: Lazy-load Monaco EditorI've completed a thorough review of the Monaco Editor lazy-loading implementation. This PR successfully moves Monaco Editor from being eagerly loaded at the app level to being lazy-loaded on demand. Here's my comprehensive analysis: Todo List:
SummaryPositive Changes:
Areas for Improvement: 🚨 Critical Issues1. Race Condition in Monaco InitializationSeverity: High The // use-monaco-store.ts:28, 42-45
let wasInitialized = false;
async setMonaco() {
if (wasInitialized) {
return;
}
wasInitialized = true;
// ... async operations
}Issue: Multiple components calling Recommendation: Use a Promise-based approach: let monacoPromise: Promise<void> | null = null;
async setMonaco() {
if (monacoPromise) {
return monacoPromise;
}
monacoPromise = this.initializeMonaco();
return monacoPromise;
}
private async initializeMonaco() {
// existing initialization logic
}2. Missing Error Handling in Async InitializationSeverity: High The // use-monaco-store.ts:46-58
const [monaco, { createHighlighter }, { shikiToMonaco }, monacoCompatibleSchema] = await Promise.all([
import('monaco-editor'),
import('shiki'),
import('@shikijs/monaco'),
import('@/lib/monaco-editor/dynamic-ref-compatible-json-schema.json'),
import('@/lib/monaco-editor/setup-monaco-workers'),
]);Issue: If any import fails, the Monaco store state becomes inconsistent, and components will be stuck showing skeleton loaders indefinitely. Recommendation: Add comprehensive error handling: async setMonaco() {
try {
// ... existing logic
} catch (error) {
console.error('Failed to initialize Monaco Editor:', error);
wasInitialized = false; // Reset for retry
// Consider setting an error state in the store
}
}3. Component Lifecycle IssuesSeverity: Medium Multiple components (MonacoEditor, CodeDiff, PromptEditor) all trigger Monaco loading independently: // Each component does this:
useEffect(() => {
setMonaco();
}, []);Issue: This creates unnecessary duplicate calls and makes the loading behavior unpredictable. Recommendation: Consider implementing a more centralized initialization strategy or ensure the Promise-based approach from issue #1 is implemented.
|
There was a problem hiding this comment.
Pull request overview
This PR implements lazy-loading for Monaco Editor to improve initial page load performance and reduce bundle size. The changes consolidate Monaco initialization into a single import function, update related packages, and refactor state management to separate concerns.
Key changes:
- Monaco Editor and Shiki packages upgraded to latest versions (monaco-editor@0.55.1, shiki@3.20.0, @shikijs/monaco@3.20.0)
- Lazy-loading implementation via
importMonaco()function that dynamically imports Monaco and dependencies only when needed - State management refactored to move
variableSuggestionsfrom monaco-store to agent-store for better separation of concerns
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Updates Monaco Editor (0.54.0 → 0.55.1), Shiki (3.13.0 → 3.20.0), and @shikijs/monaco dependencies with their transitive dependencies |
| agents-manage-ui/package.json | Updates package dependencies to match lock file versions |
| agents-manage-ui/src/features/agent/state/use-monaco-store.ts | Refactors Monaco initialization into single importMonaco() function that lazy-loads all dependencies, consolidates theme setup, and removes separate highlighter/theme methods |
| agents-manage-ui/src/features/agent/state/use-agent-store.ts | Adds variableSuggestions state and setVariableSuggestions action moved from monaco-store |
| agents-manage-ui/src/components/editors/monaco-editor.tsx | Adds importMonaco() call on mount, removes theme handling logic (now in monaco-store), and simplifies component styling |
| agents-manage-ui/src/components/agent/copilot/components/code-diff.tsx | Adds importMonaco() call on mount and removes theme handling logic |
| agents-manage-ui/src/components/editors/prompt-editor.tsx | Updates to use agentStore for variableSuggestions instead of monacoStore |
| agents-manage-ui/src/components/theme-toggle.tsx | Adds theme switching logic to update Monaco theme when system theme changes |
| agents-manage-ui/src/lib/monaco-editor/monaco-utils.ts | Updates token type matching from specific JSON types to generic types (constant, string) |
| agents-manage-ui/src/lib/tests/monaco-utils.test.tsx | Updates test to initialize Monaco before describe block and uses file snapshot for tokenization test |
| agents-manage-ui/src/lib/tests/markers-tokenize.snapshot | New snapshot file for tokenization output with updated token types |
| agents-manage-ui/src/constants/theme.ts | Renames Monaco theme constants from 'github-light-default'/'github-dark-default' to 'inkeep-light'/'inkeep-dark' |
| agents-manage-ui/src/app/[tenantId]/layout.tsx | Removes Monaco initialization and theme setup (now handled in components), simplifies to just layout wrapper |
| agents-manage-ui/src/app/[tenantId]/projects/[projectId]/agents/[agentId]/layout.tsx | Moves context suggestions logic from parent layout, adds setVariableSuggestions call |
| agents-manage-ui/src/components/agent/copilot/components/diff-viewer.tsx | Removes commented-out import |
| agents-manage-ui/cypress/support/commands.ts | Updates assertMonacoContent to use Monaco API directly instead of parsing DOM content |
| .changeset/nine-zoos-sneeze.md | Changeset documenting the fixes and lazy-loading feature |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import('@/lib/monaco-editor/dynamic-ref-compatible-json-schema.json', { | ||
| with: { | ||
| type: 'json', | ||
| }, | ||
| }), |
There was a problem hiding this comment.
The import assertion syntax using with may not be supported in all environments. The JSON import with { with: { type: 'json' } } is a Stage 3 proposal that requires appropriate TypeScript and bundler configuration. Verify that your build tooling (Next.js/Webpack/etc.) supports this syntax, or consider using a traditional import or require statement for the JSON file to ensure compatibility.
| jsonSchemaMode: false, | ||
| isSidebarPinnedOpen: true, | ||
| hasTextWrap: true, | ||
| variableSuggestions: [], |
There was a problem hiding this comment.
The variableSuggestions array is duplicated in the state initialization. It's defined in initialAgentState at line 150 and then again in the state creator at line 158. This duplication could lead to confusion and maintenance issues. Since initialAgentState is spread into the state creator, the duplicate assignment is redundant.
| let wasInitialized = false; | ||
|
|
||
| const monacoState: StateCreator<MonacoState> = (set, get) => ({ | ||
| ...initialMonacoState, | ||
| // Separate "namespace" for actions | ||
| const monacoState: StateCreator<MonacoState> = (set) => ({ | ||
| monaco: null, | ||
| actions: { | ||
| setVariableSuggestions(variableSuggestions) { | ||
| set({ variableSuggestions }); | ||
| }, | ||
| setMonacoTheme(isDark) { | ||
| const monaco = get().monaco; | ||
| if (!monaco) return; | ||
|
|
||
| // Define custom themes with blue diff colors to match TextDiff | ||
| monaco.editor.defineTheme('github-light-default', { | ||
| base: 'vs', | ||
| inherit: true, | ||
| rules: [], | ||
| colors: { | ||
| 'diffEditor.insertedTextBackground': '#3784ff19', | ||
| 'diffEditor.insertedLineBackground': '#3784ff0d', | ||
| 'scrollbarSlider.background': '#ccc5', | ||
| 'scrollbarSlider.hoverBackground': '#bbb5', | ||
| 'scrollbarSlider.activeBackground': '#aaa5', | ||
| }, | ||
| }); | ||
|
|
||
| monaco.editor.defineTheme('github-dark-default', { | ||
| base: 'vs-dark', | ||
| inherit: true, | ||
| rules: [], | ||
| colors: { | ||
| 'diffEditor.insertedTextBackground': '#69a3ff4d', | ||
| 'diffEditor.insertedLineBackground': '#69a3ff33', | ||
| 'scrollbarSlider.background': '#aaa5', | ||
| 'scrollbarSlider.hoverBackground': '#bbb5', | ||
| 'scrollbarSlider.activeBackground': '#ccc5', | ||
| }, | ||
| }); | ||
|
|
||
| const monacoTheme = isDark ? MONACO_THEME_NAME.dark : MONACO_THEME_NAME.light; | ||
| monaco.editor.setTheme(monacoTheme); | ||
| }, | ||
| async setupHighlighter(isDark) { | ||
| const { highlighter: prevHighlighter, monaco, actions } = get(); | ||
| if (!monaco) return; | ||
|
|
||
| const highlighter = prevHighlighter ?? (await getHighlighter()); | ||
| // Create the highlighter | ||
| // Register the themes from Shiki, and provide syntax highlighting for Monaco. | ||
| shikiToMonaco(highlighter, monaco); | ||
|
|
||
| // setMonacoTheme will define the custom themes and apply the correct one | ||
| actions.setMonacoTheme(isDark); | ||
| if (!prevHighlighter) { | ||
| set({ highlighter }); | ||
| highlighterPromise = null; | ||
| async importMonaco() { | ||
| if (wasInitialized) { | ||
| return; | ||
| } | ||
| }, | ||
| async setMonaco() { | ||
| const monaco = await import('monaco-editor'); | ||
| // for cypress | ||
| window.monaco = monaco; | ||
| set({ monaco }); | ||
| wasInitialized = true; |
There was a problem hiding this comment.
The module-level wasInitialized flag creates a potential race condition when multiple components call importMonaco() simultaneously. If two components call this method before the first one completes, both will pass the if (wasInitialized) check and proceed to import, causing duplicate initialization.
Consider using a Promise-based approach or a more robust synchronization mechanism to ensure only one initialization occurs, similar to how the old code used highlighterPromise.
agents-manage-ui/src/app/[tenantId]/projects/[projectId]/agents/[agentId]/layout.tsx
Show resolved
Hide resolved
| useEffect(() => { | ||
| const isDark = resolvedTheme === 'dark'; | ||
| const monacoTheme = isDark ? MONACO_THEME_NAME.dark : MONACO_THEME_NAME.light; | ||
| monaco?.editor.setTheme(monacoTheme); | ||
| }, [resolvedTheme, monaco]); |
There was a problem hiding this comment.
The useEffect will try to set the Monaco theme even before Monaco is loaded (when monaco is null). While the optional chaining protects against errors, this creates unnecessary re-renders and effect executions. Consider adding an early return if monaco is null, or only setting up the effect once Monaco is available.
| describe('Monaco-Editor Functionality', async () => { | ||
| let editor: Monaco.editor.IStandaloneCodeEditor; | ||
| let model: Monaco.editor.ITextModel; | ||
| let container: HTMLDivElement; | ||
| await monacoStore.getState().actions.importMonaco(); | ||
| // biome-ignore lint/style/noNonNullAssertion: was set after importMonaco | ||
| const monaco = monacoStore.getState().monaco!; |
There was a problem hiding this comment.
The describe callback should not be async. According to Jest/Vitest best practices, test suite callbacks (describe, beforeEach, afterEach) should be synchronous. The async initialization of Monaco should be moved into a beforeAll hook instead.
This will prevent potential race conditions and ensure proper test suite initialization.
| case 'string.value.json': | ||
| case 'number.json': | ||
| case 'keyword.json': | ||
| case 'constant': |
There was a problem hiding this comment.
@dimaMachina Is this related to lazy load, is it required for the package upgrades or is it auxiliary?
It's fine to keep this in this PR in any case, but I would like to know the reason to make sure this is desired behavior.
There was a problem hiding this comment.
it's for unit test of copy field value feature in readonly json editor
tests pass, it's safe
agents/agents-manage-ui/src/lib/__tests__/monaco-utils.test.tsx
Lines 115 to 132 in 4525c03
No description provided.