Skip to content

Commit 0c2c1ef

Browse files
committed
Update lexical tools for additional blocks and robustness
1 parent 9806392 commit 0c2c1ef

File tree

19 files changed

+1589
-195
lines changed

19 files changed

+1589
-195
lines changed

packages/lexical/src/state/LexicalAdapter.ts

Lines changed: 786 additions & 12 deletions
Large diffs are not rendered by default.

packages/lexical/src/state/LexicalState.ts

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export type InsertBlockMutation = {
4545
id: string; // lexical document ID
4646
type: string;
4747
source: string;
48-
properties?: Record<string, unknown>;
48+
metadata?: Record<string, unknown>;
4949
afterId: string;
5050
};
5151

@@ -54,7 +54,7 @@ export type InsertBlocksMutation = {
5454
blocks: Array<{
5555
type: string;
5656
source: string;
57-
properties?: Record<string, unknown>;
57+
metadata?: Record<string, unknown>;
5858
}>;
5959
afterId: string;
6060
};
@@ -64,7 +64,7 @@ export type UpdateBlockMutation = {
6464
blockId: string;
6565
type?: string;
6666
source?: string;
67-
properties?: Record<string, unknown>;
67+
metadata?: Record<string, unknown>;
6868
};
6969

7070
export type DeleteBlockMutation = {
@@ -91,15 +91,15 @@ export type LexicalState = ILexicalsState & {
9191
id: string,
9292
type?: string,
9393
source?: string,
94-
properties?: Record<string, unknown>,
94+
metadata?: Record<string, unknown>,
9595
afterId?: string,
96-
) => Promise<void>;
96+
) => Promise<OperationResult>;
9797
updateBlock: (
9898
id: string,
9999
blockId?: string,
100100
type?: string,
101101
source?: string,
102-
properties?: Record<string, unknown>,
102+
metadata?: Record<string, unknown>,
103103
) => Promise<void>;
104104
deleteBlock: (id: string, blockId?: string) => Promise<void>;
105105
readBlock: (id: string, blockId?: string) => Promise<LexicalBlock | null>;
@@ -120,6 +120,12 @@ export type LexicalState = ILexicalsState & {
120120

121121
// Additional utility methods
122122
getBlockCount: (id: string) => Promise<number>;
123+
listAvailableBlocks: (id: string) => Promise<{
124+
success: boolean;
125+
types?: any[];
126+
count?: number;
127+
error?: string;
128+
}>;
123129
reset: () => void;
124130
};
125131

@@ -145,14 +151,14 @@ export const lexicalStore = createStore<LexicalState>((set, get) => ({
145151
id: string,
146152
type?: string,
147153
source?: string,
148-
properties?: Record<string, unknown>,
154+
metadata?: Record<string, unknown>,
149155
afterId?: string,
150-
): Promise<void> => {
156+
): Promise<OperationResult> => {
151157
// Accept object from executor, destructure it
152158
const params =
153159
typeof id === 'object'
154160
? (id as any)
155-
: { id, type, source, properties, afterId };
161+
: { id, type, source, metadata, afterId };
156162

157163
const adapter = get().lexicals.get(params.id)?.adapter;
158164
if (!adapter) {
@@ -163,27 +169,30 @@ export const lexicalStore = createStore<LexicalState>((set, get) => ({
163169
block_id: '', // Will be assigned by editor
164170
block_type: params.type,
165171
source: params.source,
166-
metadata: params.properties,
172+
metadata: params.metadata,
167173
};
168174

169175
const result = await adapter.insertBlock(block, params.afterId);
170176
if (!result.success) {
171177
throw new Error(result.error || 'Failed to insert block');
172178
}
179+
180+
// Return the result with blockId
181+
return result;
173182
},
174183

175184
updateBlock: async (
176185
id: string,
177186
blockId?: string,
178187
type?: string,
179188
source?: string,
180-
properties?: Record<string, unknown>,
189+
metadata?: Record<string, unknown>,
181190
): Promise<void> => {
182191
// Accept object from executor, destructure it
183192
const params =
184193
typeof id === 'object'
185194
? (id as any)
186-
: { id, blockId, type, source, properties };
195+
: { id, blockId, type, source, metadata };
187196

188197
const adapter = get().lexicals.get(params.id)?.adapter;
189198
if (!adapter) {
@@ -203,7 +212,7 @@ export const lexicalStore = createStore<LexicalState>((set, get) => ({
203212
source: params.source ?? existingBlock.source,
204213
metadata: {
205214
...existingBlock.metadata,
206-
...params.properties,
215+
...params.metadata,
207216
},
208217
};
209218

@@ -321,6 +330,45 @@ export const lexicalStore = createStore<LexicalState>((set, get) => ({
321330
return blocks.length;
322331
},
323332

333+
listAvailableBlocks: async (
334+
id: string,
335+
): Promise<{
336+
success: boolean;
337+
types?: any[];
338+
count?: number;
339+
error?: string;
340+
}> => {
341+
console.log('[LexicalState] 🔍 listAvailableBlocks CALLED with:', { id });
342+
343+
// Delegate to adapter (following consistent pattern with all other operations)
344+
const params = typeof id === 'object' ? id : { id };
345+
console.log('[LexicalState] 📦 Processed params:', params);
346+
347+
// Special case: this operation is static and doesn't require a document
348+
// If no document is found, call the operation directly
349+
const adapter = get().lexicals.get(params.id as string)?.adapter;
350+
console.log('[LexicalState] 🔧 Adapter found?', !!adapter);
351+
352+
if (!adapter) {
353+
console.log('[LexicalState] 🚀 Calling operation directly (no adapter)');
354+
// Call operation directly without adapter (static operation)
355+
const { listAvailableBlocksOperation } =
356+
await import('../tools/operations/listAvailableBlocks');
357+
console.log('[LexicalState] 📥 Operation imported, executing...');
358+
const result = await listAvailableBlocksOperation.execute(
359+
{ type: 'all' },
360+
{ documentId: 'static', executor: null as any },
361+
);
362+
console.log('[LexicalState] ✅ Operation result:', result);
363+
return result;
364+
}
365+
366+
console.log('[LexicalState] 🔗 Delegating to adapter');
367+
const result = await adapter.listAvailableBlocks();
368+
console.log('[LexicalState] ✅ Adapter result:', result);
369+
return result;
370+
},
371+
324372
reset: () => set({ lexicals: new Map() }),
325373
}));
326374

packages/lexical/src/tools/core/executor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export class DefaultExecutor implements ToolExecutor {
5858
operationName: string,
5959
args?: unknown,
6060
): Promise<T> {
61+
console.log('[DefaultExecutor] 🚀 execute CALLED');
62+
console.log('[DefaultExecutor] Operation:', operationName);
63+
console.log('[DefaultExecutor] Args:', args);
64+
console.log('[DefaultExecutor] LexicalId:', this.lexicalId);
65+
6166
// Get the store method directly (1:1 mapping, no transformation)
6267
const method = (this.store as unknown as Record<string, unknown>)[
6368
operationName
@@ -78,12 +83,16 @@ export class DefaultExecutor implements ToolExecutor {
7883
? { id: this.lexicalId, ...args }
7984
: { id: this.lexicalId };
8085

86+
console.log('[DefaultExecutor] 📦 Payload to store method:', payload);
87+
8188
try {
89+
console.log('[DefaultExecutor] 📞 Calling store method...');
8290
const result = await (method as (args: unknown) => Promise<T>).call(
8391
this.store,
8492
payload,
8593
);
8694

95+
console.log('[DefaultExecutor] ✅ Store method returned:', result);
8796
return result;
8897
} catch (error) {
8998
console.error('[DefaultExecutor] ❌ ERROR calling store method:', error);

packages/lexical/src/tools/core/types.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,18 +145,14 @@ export interface LexicalBlock {
145145
export type BlockFormat = 'brief' | 'detailed';
146146

147147
/**
148-
* Brief block representation for structure queries
149-
* Includes block_id, block_type, and a 40-char content preview
148+
* Brief block representation for structure queries (CSV-serializable)
149+
* Fields: block_id, block_type, preview, collapsible
150150
*/
151151
export interface BriefBlock {
152-
/** Unique block identifier */
153152
block_id: string;
154-
155-
/** Block type identifier */
156153
block_type: string;
157-
158-
/** 40-character preview of block content (empty for horizontalrule) */
159154
preview: string;
155+
collapsible?: string;
160156
}
161157

162158
/**

packages/lexical/src/tools/definitions/insertBlock.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { insertBlockParamsSchema } from '../schemas/insertBlock';
1919
*
2020
* Supports inserting various block types:
2121
* - paragraph: Regular text paragraph
22-
* - heading: Semantic HTML heading (NOT markdown - use plain text, specify tag property for h1-h6)
23-
* - code: Code block (specify language in properties)
22+
* - heading: Semantic HTML heading (NOT markdown - use plain text, specify tag in metadata for h1-h6)
23+
* - code: Code block (specify language in metadata)
2424
* - quote: Blockquote
2525
* - list/listitem: List blocks
2626
* - jupyter-cell: Executable Jupyter code cells
@@ -30,7 +30,7 @@ export const insertBlockTool: ToolDefinition = {
3030
displayName: 'Insert Lexical Block',
3131
toolReferenceName: 'insertBlock',
3232
description:
33-
"Insert different type of content with blocks. Use listAvailableBlocks to get availables blocks. When inserting MULTIPLE blocks sequentially (e.g., creating a document outline with heading + paragraph + code), ALWAYS use afterId: 'BOTTOM' for each insertion to append blocks in order. For single insertions, call readAllBlocks first to see document structure. Position blocks using afterId: 'TOP' (beginning), 'BOTTOM' (end - REQUIRED for sequential inserts), or a specific block_id value from readAllBlocks. IMPORTANT: heading blocks are semantic HTML (NOT markdown) - use plain text in source field without # symbols, specify tag property (h1-h6) instead. Use listAvailableBlocks to see all supported types. Works on active .lexical file.",
33+
"CRITICAL: DO NOT use markdown syntax in source field. Heading blocks use PLAIN TEXT (no # symbols) - specify tag metadata (h1-h6) instead. Inline formatting like **bold** or *italic* is automatically converted. ALWAYS call listAvailableBlocks FIRST to see block types and required metadata format. Insert different types of content blocks. When inserting MULTIPLE blocks sequentially (e.g., creating a document outline with heading + paragraph + code), ALWAYS use afterId: 'BOTTOM' for each insertion to append blocks in order. For single insertions, call readAllBlocks first to see document structure. Position blocks using afterId: 'TOP' (beginning), 'BOTTOM' (end - REQUIRED for sequential inserts), or a specific block_id value from readAllBlocks. To insert blocks INSIDE a collapsible: 1) First insert collapsible (type='collapsible'), which returns a blockId, 2) Then insert nested blocks with metadata.collapsible set to that RETURNED BLOCK ID (NOT 'TOP' or 'BOTTOM'). Example: result = insertBlock({type: 'collapsible', source: 'Section', afterId: 'BOTTOM'}); insertBlock({type: 'paragraph', source: 'text', metadata: {collapsible: result.blockId}, afterId: 'BOTTOM'}). DO NOT use position markers (TOP/BOTTOM) as collapsible IDs - they are only for afterId positioning. Works on active .lexical file.",
3434

3535
parameters: zodToToolParameters(insertBlockParamsSchema),
3636

packages/lexical/src/tools/definitions/listAvailableBlocks.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@ export const listAvailableBlocksTool: ToolDefinition = {
3131
displayName: 'List Available Lexical Blocks',
3232
toolReferenceName: 'listAvailableBlocks',
3333
description:
34-
"Discover available block types for the currently open Lexical document. Returns schema for all registered blocks including: 'jupyter-cell' (executable code cell with language property), standard blocks (paragraph, heading [NOT markdown - semantic HTML with tag property], code, quote, list, table). Use this to see exact block type names and required properties before calling insertBlock.",
34+
"Discover available block types for the currently open Lexical document. Available blocks: paragraph, heading, quote, code, list, horizontalrule, jupyter-cell (executable), equation, image, youtube (video embed), table (data table), collapsible (expandable section). Returns detailed schema including required/optional metadata for each block type. Set type parameter to specific block type (e.g., 'youtube', 'table') or 'all' (default) for all blocks. Use this BEFORE calling insertBlock to see exact type names and required metadata format.",
3535

3636
parameters: zodToToolParameters(listAvailableBlocksParamsSchema),
3737

3838
operation: 'listAvailableBlocks',
3939

4040
config: {
41-
confirmationMessage: (params: { category?: string }) =>
42-
params.category
43-
? `List available ${params.category} block types?`
41+
confirmationMessage: (params: { type?: string }) =>
42+
params.type && params.type !== 'all'
43+
? `List details for '${params.type}' block type?`
4444
: 'List all available block types?',
45-
invocationMessage: (params: { category?: string }) =>
46-
params.category
47-
? `Listing ${params.category} block types`
45+
invocationMessage: (params: { type?: string }) =>
46+
params.type && params.type !== 'all'
47+
? `Listing '${params.type}' block type details`
4848
: 'Listing all available block types',
4949
requiresConfirmation: false,
5050
canBeReferencedInPrompt: true,

packages/lexical/src/tools/definitions/readAllBlocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const readAllBlocksTool: ToolDefinition = {
2424
displayName: 'Read All Lexical Blocks',
2525
toolReferenceName: 'readAllBlocks',
2626
description:
27-
"Read all blocks from the currently open Lexical document. Use listAvailableBlocks to get available blocks. Supports two response formats: 'brief' (default, ~1,100 tokens) returns block_id, block_type, and 40-char content preview for structure queries; 'detailed' (~20,000 tokens) returns full content with source, metadata, and properties. Use brief when you need to see document structure, count blocks, or quickly scan content. Use detailed when you need to read full content. Brief format preview shows: lists as comma-separated items, code/jupyter-cell as first line, horizontalrule as empty string. Returns array of blocks with: block_id (stable identifier for insertion), block_type (e.g. 'heading', 'paragraph', 'jupyter-cell'), preview (brief only), and optionally source/metadata (detailed only). CRITICAL: Use the block_id values from this result for insertBlock's afterId parameter. Works on active .lexical file.",
27+
"Read all blocks from the currently open Lexical document. Use listAvailableBlocks to get available block types. Supports two response formats: 'brief' (default, ~1,100 tokens) returns block_id, block_type, and 40-char content preview for structure queries; 'detailed' (~20,000 tokens) returns full content with source, metadata, and properties. Use brief when you need to see document structure, count blocks, or quickly scan content. Use detailed when you need to read full content. Brief format preview shows: lists as comma-separated items, code/jupyter-cell as first line, horizontalrule as empty string. Returns array of blocks with: block_id (stable identifier for insertion), block_type (e.g. 'heading', 'paragraph', 'jupyter-cell'), preview (brief only), and optionally source/metadata (detailed only). CRITICAL: Use the block_id values from this result for insertBlock's afterId parameter. Works on active .lexical file.",
2828

2929
parameters: zodToToolParameters(readAllBlocksParamsSchema),
3030

packages/lexical/src/tools/definitions/readBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const readBlockTool: ToolDefinition = {
2424
displayName: 'Read Lexical Block',
2525
toolReferenceName: 'readBlock',
2626
description:
27-
"Read a single block from the currently open Lexical document by its block_id. Use listAvailableBlocks to get available blocks. Returns the block with: block_id, block_type (e.g. 'heading', 'paragraph', 'jupyter-cell'), source (content as string), metadata (properties like level, language). Use block_id values from readAllBlocks. Works on active .lexical file.",
27+
"Read a single block from the currently open Lexical document by its block_id. Use listAvailableBlocks to get available block types. Returns the block with: block_id, block_type (e.g. 'heading', 'paragraph', 'jupyter-cell'), source (content as string), metadata (properties like level, language). Use block_id values from readAllBlocks. Works on active .lexical file.",
2828

2929
parameters: zodToToolParameters(readBlockParamsSchema),
3030

packages/lexical/src/tools/definitions/updateBlock.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import { updateBlockParamsSchema } from '../schemas/updateBlock';
1717
/**
1818
* Tool definition for updating an existing block
1919
*
20-
* Modifies block type, source content, and/or properties.
20+
* Modifies block type, source content, and/or metadata.
2121
*/
2222
export const updateBlockTool: ToolDefinition = {
2323
name: 'datalayer_updateBlock',
2424
displayName: 'Update Lexical Block',
2525
toolReferenceName: 'updateBlock',
2626
description:
27-
'Update an existing block in the currently open Lexical document. Use listAvailableBlocks to get available blocks. Can modify block type, source content, and/or properties. Requires id from readAllBlocks. At least one of type, source, or properties must be provided. Properties are merged with existing metadata. Works on active .lexical file.',
27+
'Update an existing block in the currently open Lexical document. CRITICAL: DO NOT use markdown syntax (like # for headings) in source field - use plain text and specify block type/metadata instead. Inline formatting like **bold** or *italic* is automatically converted. Use listAvailableBlocks to get available block types. Can modify block type, source content, and/or metadata. Requires id from readAllBlocks. At least one of type, source, or metadata must be provided. Metadata is merged with existing metadata. Works on active .lexical file.',
2828

2929
parameters: zodToToolParameters(updateBlockParamsSchema),
3030

@@ -35,12 +35,12 @@ export const updateBlockTool: ToolDefinition = {
3535
id: string;
3636
type?: string;
3737
source?: string;
38-
properties?: Record<string, unknown>;
38+
metadata?: Record<string, unknown>;
3939
}) => {
4040
const updates: string[] = [];
4141
if (params.type) updates.push(`type to ${params.type}`);
4242
if (params.source) updates.push('source');
43-
if (params.properties) updates.push('properties');
43+
if (params.metadata) updates.push('metadata');
4444
return `Update block ${params.id} (${updates.join(', ')})?`;
4545
},
4646
invocationMessage: (params: { id: string }) =>

packages/lexical/src/tools/operations/deleteBlock.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,18 @@ export const deleteBlockOperation: ToolOperation<
136136
// Store information about blocks before deletion
137137
const deletedBlocks: DeletedBlockInfo[] = [];
138138

139-
// Delete each block (order doesn't matter - IDs are stable)
140-
for (const id of ids) {
139+
// Sort IDs in reverse order to delete children before parents (collapsibles last)
140+
// This prevents cascading deletions from causing "block not found" errors
141+
const sortedIds = [...ids].sort((a, b) => {
142+
// Find positions in document
143+
const indexA = blocks.findIndex(block => block.block_id === a);
144+
const indexB = blocks.findIndex(block => block.block_id === b);
145+
// Sort in reverse order (higher index first)
146+
return indexB - indexA;
147+
});
148+
149+
// Delete each block in reverse order
150+
for (const id of sortedIds) {
141151
// Find and store block info before deletion
142152
const block = blocks.find(b => b.block_id === id);
143153

@@ -147,16 +157,30 @@ export const deleteBlockOperation: ToolOperation<
147157
}
148158

149159
// Execute deletion via executor
150-
await context.executor!.execute(this.name, {
151-
blockId: id,
152-
});
153-
154-
// Track deletion
155-
deletedBlocks.push({
156-
id: block.block_id,
157-
type: block.block_type,
158-
source: block.source,
159-
});
160+
// Catch "block not found" errors - block was already deleted by cascading deletion
161+
try {
162+
await context.executor!.execute(this.name, {
163+
blockId: id,
164+
});
165+
166+
// Track successful deletion
167+
deletedBlocks.push({
168+
id: block.block_id,
169+
type: block.block_type,
170+
source: block.source,
171+
});
172+
} catch (error) {
173+
// If block not found, it was likely deleted by cascading deletion (parent collapsible removed)
174+
// Skip and continue - this is expected behavior
175+
const errorMessage =
176+
error instanceof Error ? error.message : String(error);
177+
if (errorMessage.includes('not found')) {
178+
// Block was already deleted (cascaded), skip it
179+
continue;
180+
}
181+
// Re-throw other errors
182+
throw error;
183+
}
160184
}
161185

162186
// Format message similar to deleteCell pattern

0 commit comments

Comments
 (0)