Skip to content

Commit d55c8b4

Browse files
committed
Add tool definitions operations and schemas
1 parent 2de72b1 commit d55c8b4

27 files changed

+1851
-63
lines changed

packages/react/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
"access": "public"
3030
},
3131
"scripts": {
32-
"build": "gulp resources-to-lib && tsc && webpack",
33-
"build:lib": "tsc",
34-
"build:prod": "gulp resources-to-lib && tsc && npm run clean && npm run build:lib",
32+
"build": "npm run validate:tools && gulp resources-to-lib && tsc && webpack",
33+
"build:lib": "npm run validate:tools && tsc",
34+
"build:prod": "npm run validate:tools && gulp resources-to-lib && tsc && npm run clean && npm run build:lib",
3535
"build:tsc:watch:res": "gulp resources-to-lib-watch",
3636
"build:tsc:watch:tsc": "tsc --watch",
3737
"build:webpack": "cross-env BUILD_APP=true webpack-cli build",
@@ -62,6 +62,7 @@
6262
"test": "jest --coverage",
6363
"test:visual": "playwright test",
6464
"typedoc": "typedoc ./src",
65+
"validate:tools": "node scripts/validate-tools-sync.js",
6566
"watch": "run-p watch:src",
6667
"watch:src": "tsc -w"
6768
},
@@ -144,6 +145,7 @@
144145
"@lumino/default-theme": "^2.0.0",
145146
"@primer/react": "^37.19.0",
146147
"@primer/react-brand": "^0.58.2",
148+
"@toon-format/toon": "^1.3.0",
147149
"ansi-to-html": "^0.7.2",
148150
"assert": "^2.0.0",
149151
"bufferutil": "^4.0.8",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env node
2+
/*
3+
* Copyright (c) 2021-2023 Datalayer, Inc.
4+
*
5+
* MIT License
6+
*/
7+
8+
/**
9+
* Validation script to ensure tool definitions and operations stay in sync.
10+
*
11+
* Checks:
12+
* 1. Every definition has a corresponding operation
13+
* 2. Every operation has a corresponding definition
14+
* 3. Tool names match between definition and operation
15+
* 4. No orphaned files
16+
*
17+
* Usage: npm run validate:tools
18+
*/
19+
20+
const fs = require('fs');
21+
const path = require('path');
22+
23+
const TOOLS_DIR = path.join(__dirname, '../src/tools');
24+
const DEFINITIONS_DIR = path.join(TOOLS_DIR, 'definitions');
25+
const OPERATIONS_DIR = path.join(TOOLS_DIR, 'operations');
26+
27+
function getToolFiles(dir) {
28+
const files = fs.readdirSync(dir);
29+
return files
30+
.filter(f => f.endsWith('.ts') && f !== 'index.ts')
31+
.map(f => ({
32+
name: f.replace('.ts', ''),
33+
path: path.join(dir, f),
34+
}));
35+
}
36+
37+
function extractToolName(filePath) {
38+
const content = fs.readFileSync(filePath, 'utf-8');
39+
40+
// For definitions: export const xxxTool
41+
const defMatch = content.match(/export const (\w+)Tool:/);
42+
if (defMatch) {
43+
return defMatch[1]; // e.g., "insertCell" from "insertCellTool"
44+
}
45+
46+
// For operations: export const xxxOperation
47+
const opMatch = content.match(/export const (\w+)Operation:/);
48+
if (opMatch) {
49+
return opMatch[1]; // e.g., "insertCell" from "insertCellOperation"
50+
}
51+
52+
return null;
53+
}
54+
55+
function main() {
56+
console.log('🔍 Validating tool definitions and operations sync...\n');
57+
58+
const definitions = getToolFiles(DEFINITIONS_DIR);
59+
const operations = getToolFiles(OPERATIONS_DIR);
60+
61+
console.log(`Found ${definitions.length} definitions:`);
62+
definitions.forEach(d => console.log(` - ${d.name}`));
63+
console.log();
64+
65+
console.log(`Found ${operations.length} operations:`);
66+
operations.forEach(o => console.log(` - ${o.name}`));
67+
console.log();
68+
69+
let hasErrors = false;
70+
71+
// Check 1: File name alignment
72+
const defNames = new Set(definitions.map(d => d.name));
73+
const opNames = new Set(operations.map(o => o.name));
74+
75+
// Find definitions without operations
76+
const missingOps = definitions.filter(d => !opNames.has(d.name));
77+
if (missingOps.length > 0) {
78+
console.error('❌ Definitions without matching operations:');
79+
missingOps.forEach(d => console.error(` ${d.name}`));
80+
hasErrors = true;
81+
}
82+
83+
// Find operations without definitions
84+
const missingDefs = operations.filter(o => !defNames.has(o.name));
85+
if (missingDefs.length > 0) {
86+
console.error('❌ Operations without matching definitions:');
87+
missingDefs.forEach(o => console.error(` ${o.name}`));
88+
hasErrors = true;
89+
}
90+
91+
// Check 2: Tool name extraction from file content
92+
console.log('\n📝 Checking exported names...');
93+
for (const def of definitions) {
94+
const toolName = extractToolName(def.path);
95+
if (!toolName) {
96+
console.error(`❌ Could not extract tool name from ${def.name}`);
97+
hasErrors = true;
98+
} else if (toolName !== def.name) {
99+
console.error(
100+
`❌ File name mismatch: ${def.name}.ts exports ${toolName}Tool`
101+
);
102+
hasErrors = true;
103+
}
104+
}
105+
106+
for (const op of operations) {
107+
const toolName = extractToolName(op.path);
108+
if (!toolName) {
109+
console.error(`❌ Could not extract tool name from ${op.name}`);
110+
hasErrors = true;
111+
} else if (toolName !== op.name) {
112+
console.error(
113+
`❌ File name mismatch: ${op.name}.ts exports ${toolName}Operation`
114+
);
115+
hasErrors = true;
116+
}
117+
}
118+
119+
// Summary
120+
console.log('\n' + '='.repeat(60));
121+
if (hasErrors) {
122+
console.error('❌ Validation FAILED - tools are out of sync!');
123+
process.exit(1);
124+
} else {
125+
console.log('✅ Validation PASSED - all tools are in sync!');
126+
console.log(`\n ${definitions.length} definition(s)`);
127+
console.log(` ${operations.length} operation(s)`);
128+
console.log(' Perfect 1:1 alignment! 🎉');
129+
}
130+
}
131+
132+
main();

packages/react/src/components/notebook/Notebook2Adapter.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { Context } from '@jupyterlab/docregistry';
1414
import { NotebookModel } from '@jupyterlab/notebook';
1515
import * as nbformat from '@jupyterlab/nbformat';
16+
import { NotebookCommandIds } from './NotebookCommands';
1617

1718
export class Notebook2Adapter {
1819
private _commands: CommandRegistry;
@@ -198,6 +199,209 @@ export class Notebook2Adapter {
198199
NotebookActions.redo(notebook);
199200
}
200201

202+
/**
203+
* Set the active cell by index.
204+
*
205+
* @param index - The index of the cell to activate (0-based)
206+
*
207+
* @remarks
208+
* This method programmatically selects a cell at the specified index.
209+
* If the index is out of bounds, the operation has no effect.
210+
*/
211+
setActiveCell(index: number): void {
212+
const notebook = this._notebook;
213+
const cellCount = notebook.model?.cells.length ?? 0;
214+
215+
if (index >= 0 && index < cellCount) {
216+
notebook.activeCellIndex = index;
217+
}
218+
}
219+
220+
/**
221+
* Get all cells from the notebook.
222+
*
223+
* @returns Array of cell data including type, source, and outputs
224+
*
225+
* @remarks
226+
* This method extracts cell information from the notebook model.
227+
* For code cells, outputs are included. Returns an empty array if
228+
* the notebook model is not available.
229+
*/
230+
getCells(): Array<{
231+
type: nbformat.CellType;
232+
source: string;
233+
outputs?: unknown[];
234+
}> {
235+
const cells = this._notebook.model?.cells;
236+
if (!cells) {
237+
return [];
238+
}
239+
240+
const result: Array<{
241+
type: nbformat.CellType;
242+
source: string;
243+
outputs?: unknown[];
244+
}> = [];
245+
246+
for (let i = 0; i < cells.length; i++) {
247+
const cell = cells.get(i);
248+
if (cell) {
249+
result.push({
250+
type: cell.type as nbformat.CellType,
251+
source: cell.sharedModel.getSource(),
252+
outputs:
253+
cell.type === 'code' ? (cell as any).outputs?.toJSON() : undefined,
254+
});
255+
}
256+
}
257+
258+
return result;
259+
}
260+
261+
/**
262+
* Get the total number of cells in the notebook.
263+
*
264+
* @returns The number of cells, or 0 if the notebook model is not available
265+
*/
266+
getCellCount(): number {
267+
return this._notebook.model?.cells.length ?? 0;
268+
}
269+
270+
/**
271+
* Insert a new cell at a specific index.
272+
*
273+
* @param cellIndex - The index where the cell should be inserted (0-based)
274+
* @param source - Optional source code/text for the new cell
275+
*
276+
* @remarks
277+
* This method inserts a cell at the specified position by:
278+
* 1. Setting the active cell to cellIndex
279+
* 2. Calling insertAbove to insert before that cell
280+
*
281+
* Note: The cell type is determined by _defaultCellType, which should be set
282+
* before calling this method (typically done by the store layer).
283+
*/
284+
insertAt(cellIndex: number, source?: string): void {
285+
const cellCount = this.getCellCount();
286+
287+
console.log('[Notebook2Adapter.insertAt] BEFORE:', {
288+
cellIndex,
289+
sourceLength: source?.length || 0,
290+
sourcePreview: source?.substring(0, 50),
291+
currentActiveCell: this._notebook.activeCellIndex,
292+
cellCount,
293+
defaultCellType: this._defaultCellType,
294+
});
295+
296+
// If index is beyond cell count, insert at the end
297+
if (cellIndex >= cellCount) {
298+
console.log(
299+
'[Notebook2Adapter.insertAt] Index beyond cell count, inserting at end'
300+
);
301+
this.insertBelow(source);
302+
return;
303+
}
304+
305+
this.setActiveCell(cellIndex);
306+
307+
console.log('[Notebook2Adapter.insertAt] AFTER setActiveCell:', {
308+
newActiveCell: this._notebook.activeCellIndex,
309+
});
310+
311+
this.insertAbove(source);
312+
313+
console.log('[Notebook2Adapter.insertAt] AFTER insertAbove:', {
314+
cellCount: this.getCellCount(),
315+
newActiveCell: this._notebook.activeCellIndex,
316+
});
317+
}
318+
319+
/**
320+
* NEW ALIGNED TOOL METHODS
321+
* These methods align 1:1 with tool operation names for seamless integration
322+
*/
323+
324+
/**
325+
* Insert a cell at a specific index (aligned with insertCell tool).
326+
*
327+
* @param cellType - Type of cell to insert (code, markdown, or raw)
328+
* @param cellIndex - Index where to insert (0-based). Use large number for end.
329+
* @param source - Optional source code/text for the cell
330+
*/
331+
insertCell(
332+
cellType: nbformat.CellType,
333+
cellIndex: number,
334+
source?: string
335+
): void {
336+
this.setDefaultCellType(cellType);
337+
this.insertAt(cellIndex, source);
338+
}
339+
340+
/**
341+
* Delete the currently active cell (aligned with deleteCell tool).
342+
*/
343+
deleteCell(): void {
344+
this.deleteCells();
345+
}
346+
347+
/**
348+
* Update a cell's content and/or type (aligned with updateCell tool).
349+
*
350+
* @param cellType - New cell type
351+
* @param source - New source content (optional)
352+
*/
353+
updateCell(cellType: nbformat.CellType, source?: string): void {
354+
if (source !== undefined && this._notebook.activeCell) {
355+
this._notebook.activeCell.model.sharedModel.setSource(source);
356+
}
357+
this.changeCellType(cellType);
358+
}
359+
360+
/**
361+
* Get a cell's content by index or active cell (aligned with getCell tool).
362+
*
363+
* @param index - Optional cell index (0-based). If not provided, returns active cell.
364+
* @returns Cell data or undefined if not found
365+
*/
366+
getCell(
367+
index?: number
368+
):
369+
| { type: nbformat.CellType; source: string; outputs?: unknown[] }
370+
| undefined {
371+
if (index !== undefined) {
372+
// Get cell at specific index
373+
const cells = this.getCells();
374+
return cells[index];
375+
} else {
376+
// Get active cell
377+
const activeCell = this._notebook.activeCell;
378+
if (!activeCell) return undefined;
379+
380+
return {
381+
type: activeCell.model.type,
382+
source: activeCell.model.sharedModel.getSource(),
383+
outputs:
384+
activeCell.model.type === 'code'
385+
? (activeCell.model as any).outputs?.toJSON()
386+
: undefined,
387+
};
388+
}
389+
}
390+
391+
/**
392+
* Run the active cell (aligned with runCell tool).
393+
*/
394+
runCell(): void {
395+
this._commands.execute(NotebookCommandIds.run);
396+
}
397+
398+
/**
399+
* Run all cells in the notebook (aligned with runAllCells tool).
400+
*/
401+
runAllCells(): void {
402+
this._commands.execute(NotebookCommandIds.runAll);
403+
}
404+
201405
/**
202406
* Dispose of the adapter.
203407
*/

0 commit comments

Comments
 (0)