Skip to content

Commit b23af49

Browse files
authored
Positron notebooks - Add new functionality to edit tool for moving cells. (#11023)
Addresses #10553. ### Summary This PR adds notebook cell reordering capabilities to the Positron Assistant, enabling users to reorganize cells through natural language prompts like "move all imports to the top cell" or "move the third cell to the bottom." https://github.com/user-attachments/assets/0e4835d6-1466-4ad4-8541-514bcdffd2d4 The implementation extends the `EditNotebookCellsTool` with a new `reorder` operation that supports both single cell moves (via `fromIndex`/`toIndex`) and full permutation reordering (via `newOrder` array). The feature includes validation for permutations and indices, confirmation prompts for the user, and integration with the notebook editor through the `positron.notebooks.reorderCells` API. ### Release Notes #### New Features - N/A #### Bug Fixes - N/A ### QA Notes @:positron-notebooks @:assistant 1. Open a Jupyter notebook with multiple cells 2. Open the Positron Assistant 3. Test single cell moves. E.g. "Move the last cell to the top" 4. Test batch reordering. E.g. "Move all import statements to the top cell"
1 parent 2da57d7 commit b23af49

File tree

8 files changed

+401
-12
lines changed

8 files changed

+401
-12
lines changed

extensions/positron-assistant/package.json

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@
845845
{
846846
"name": "editNotebookCells",
847847
"displayName": "Edit Notebook Cells",
848-
"modelDescription": "Perform edit operations on notebook cells. Use operation='add' to create new cells (requires cellType, index, and content). Code cells are executed by default after adding (set run=false to skip execution). Use operation='update' to modify existing cells (requires cellIndex and content), or operation='delete' to remove cells (requires cellIndex). Use index=-1 to append cells at the end when adding.",
848+
"modelDescription": "Perform edit operations on notebook cells. Use operation='add' to create new cells (requires cellType, index, and content). Code cells are executed by default after adding (set run=false to skip execution). Use operation='update' to modify existing cells (requires cellIndex and content), operation='delete' to remove cells (requires cellIndex), or operation='reorder' to reorganize cells. For reorder, either use fromIndex and toIndex to move a single cell, or provide newOrder array (a permutation where newOrder[i] is the original index of the cell that should be at position i) to reorder all cells at once. Use index=-1 to append cells at the end when adding.",
849849
"toolReferenceName": "editNotebookCells",
850850
"canBeReferencedInPrompt": true,
851851
"tags": [
@@ -860,9 +860,10 @@
860860
"enum": [
861861
"add",
862862
"update",
863-
"delete"
863+
"delete",
864+
"reorder"
864865
],
865-
"description": "Type of edit operation to perform. 'add' creates a new cell, 'update' modifies existing cell content, 'delete' removes a cell."
866+
"description": "Type of edit operation to perform. 'add' creates a new cell, 'update' modifies existing cell content, 'delete' removes a cell, 'reorder' moves cells to new positions."
866867
},
867868
"cellType": {
868869
"type": "string",
@@ -887,6 +888,21 @@
887888
"run": {
888889
"type": "boolean",
889890
"description": "Whether to execute the cell after adding it. Defaults to true for code cells. Ignored for markdown cells."
891+
},
892+
"fromIndex": {
893+
"type": "number",
894+
"description": "Current index of the cell to move (0-based). Required when operation is 'reorder' and not using newOrder."
895+
},
896+
"toIndex": {
897+
"type": "number",
898+
"description": "Target index to move the cell to (0-based). Required when operation is 'reorder' and not using newOrder."
899+
},
900+
"newOrder": {
901+
"type": "array",
902+
"items": {
903+
"type": "number"
904+
},
905+
"description": "Array specifying new cell order. newOrder[i] is the original index of the cell that should be at position i. Must be a valid permutation containing each index 0 to cellCount-1 exactly once. Use this for full notebook reordering. Alternative to fromIndex/toIndex for operation='reorder'."
890906
}
891907
},
892908
"required": [

extensions/positron-assistant/src/tools/notebookTools.ts

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode';
77
import * as positron from 'positron';
88
import { PositronAssistantToolName } from '../types.js';
99
import { log } from '../extension.js';
10-
import { convertOutputsToLanguageModelParts, formatCells, validateCellIndices, MAX_CELL_CONTENT_LENGTH } from './notebookUtils.js';
10+
import { convertOutputsToLanguageModelParts, formatCells, validateCellIndices, validatePermutation, MAX_CELL_CONTENT_LENGTH } from './notebookUtils.js';
1111

1212
/**
1313
* Gets the active notebook context, returning null if no notebook is active.
@@ -144,16 +144,19 @@ export const RunNotebookCellsTool = vscode.lm.registerTool<{
144144
/**
145145
* Tool: Edit Notebook Cells
146146
*
147-
* Performs edit operations on notebook cells: add, update, or delete.
147+
* Performs edit operations on notebook cells: add, update, delete, or reorder.
148148
* Uses a simple enum-based operation parameter for flexibility.
149149
*/
150150
export const EditNotebookCellsTool = vscode.lm.registerTool<{
151-
operation: 'add' | 'update' | 'delete';
151+
operation: 'add' | 'update' | 'delete' | 'reorder';
152152
cellType?: 'code' | 'markdown';
153153
index?: number;
154154
content?: string;
155155
cellIndex?: number;
156156
run?: boolean;
157+
fromIndex?: number;
158+
toIndex?: number;
159+
newOrder?: number[];
157160
}>(PositronAssistantToolName.EditNotebookCells, {
158161
prepareInvocation: async (options, _token) => {
159162
const { operation, cellType, cellIndex, run } = options.input;
@@ -176,6 +179,10 @@ export const EditNotebookCellsTool = vscode.lm.registerTool<{
176179
invocationMessage: vscode.l10n.t('Deleting notebook cell'),
177180
pastTenseMessage: vscode.l10n.t('Deleted notebook cell'),
178181
},
182+
reorder: {
183+
invocationMessage: vscode.l10n.t('Reordering notebook cells'),
184+
pastTenseMessage: vscode.l10n.t('Reordered notebook cells'),
185+
},
179186
};
180187
return messages[operation];
181188
}
@@ -240,6 +247,35 @@ export const EditNotebookCellsTool = vscode.lm.registerTool<{
240247
};
241248
}
242249

250+
case 'reorder': {
251+
const { fromIndex, toIndex, newOrder } = options.input;
252+
253+
// Determine if this is a single move or full reorder
254+
if (newOrder !== undefined) {
255+
// Full permutation reorder
256+
const message = vscode.l10n.t('Reorder all {0} cells in the notebook?', context.cellCount);
257+
return {
258+
invocationMessage: vscode.l10n.t('Reordering notebook cells'),
259+
confirmationMessages: {
260+
title: vscode.l10n.t('Reorder Notebook Cells'),
261+
message: message
262+
},
263+
pastTenseMessage: vscode.l10n.t('Reordered notebook cells'),
264+
};
265+
} else {
266+
// Single cell move
267+
const message = vscode.l10n.t('Move cell {0} to position {1}?', fromIndex, toIndex);
268+
return {
269+
invocationMessage: vscode.l10n.t('Moving notebook cell'),
270+
confirmationMessages: {
271+
title: vscode.l10n.t('Move Notebook Cell'),
272+
message: message
273+
},
274+
pastTenseMessage: vscode.l10n.t('Moved notebook cell'),
275+
};
276+
}
277+
}
278+
243279
default:
244280
return {
245281
invocationMessage: vscode.l10n.t('Editing notebook cell'),
@@ -248,7 +284,7 @@ export const EditNotebookCellsTool = vscode.lm.registerTool<{
248284
}
249285
},
250286
invoke: async (options, token) => {
251-
const { operation, cellType, index, content, cellIndex, run } = options.input;
287+
const { operation, cellType, index, content, cellIndex, run, fromIndex, toIndex, newOrder } = options.input;
252288

253289
try {
254290
const context = await getActiveNotebookContext();
@@ -419,10 +455,76 @@ export const EditNotebookCellsTool = vscode.lm.registerTool<{
419455
]);
420456
}
421457

458+
case 'reorder': {
459+
// Determine if this is a single move or full reorder
460+
if (newOrder !== undefined) {
461+
// Full permutation reorder
462+
const permValidation = validatePermutation(newOrder, context.cellCount);
463+
if (!permValidation.valid) {
464+
return new vscode.LanguageModelToolResult([
465+
new vscode.LanguageModelTextPart(permValidation.error!)
466+
]);
467+
}
468+
469+
// Check for identity permutation (no-op)
470+
if (permValidation.isIdentity) {
471+
return new vscode.LanguageModelToolResult([
472+
new vscode.LanguageModelTextPart('No reordering needed - cells are already in the specified order')
473+
]);
474+
}
475+
476+
await positron.notebooks.reorderCells(context.uri, newOrder);
477+
478+
return new vscode.LanguageModelToolResult([
479+
new vscode.LanguageModelTextPart(`Successfully reordered ${context.cellCount} cells`)
480+
]);
481+
} else {
482+
// Single cell move - validate required parameters
483+
if (fromIndex === undefined) {
484+
return new vscode.LanguageModelToolResult([
485+
new vscode.LanguageModelTextPart('Missing required parameter: fromIndex (current index of cell to move)')
486+
]);
487+
}
488+
if (toIndex === undefined) {
489+
return new vscode.LanguageModelToolResult([
490+
new vscode.LanguageModelTextPart('Missing required parameter: toIndex (target index to move cell to)')
491+
]);
492+
}
493+
494+
// Validate indices
495+
const fromValidation = validateCellIndices([fromIndex], context.cellCount);
496+
if (!fromValidation.valid) {
497+
return new vscode.LanguageModelToolResult([
498+
new vscode.LanguageModelTextPart(`Invalid fromIndex: ${fromValidation.error}`)
499+
]);
500+
}
501+
502+
const toValidation = validateCellIndices([toIndex], context.cellCount);
503+
if (!toValidation.valid) {
504+
return new vscode.LanguageModelToolResult([
505+
new vscode.LanguageModelTextPart(`Invalid toIndex: ${toValidation.error}`)
506+
]);
507+
}
508+
509+
// Check for no-op
510+
if (fromIndex === toIndex) {
511+
return new vscode.LanguageModelToolResult([
512+
new vscode.LanguageModelTextPart('No move needed - cell is already at the specified position')
513+
]);
514+
}
515+
516+
await positron.notebooks.moveCell(context.uri, fromIndex, toIndex);
517+
518+
return new vscode.LanguageModelToolResult([
519+
new vscode.LanguageModelTextPart(`Successfully moved cell from index ${fromIndex} to index ${toIndex}`)
520+
]);
521+
}
522+
}
523+
422524
default:
423525
return new vscode.LanguageModelToolResult([
424526
new vscode.LanguageModelTextPart(
425-
`Unknown operation: ${operation}. Must be "add", "update", or "delete".`
527+
`Unknown operation: ${operation}. Must be "add", "update", "delete", or "reorder".`
426528
)
427529
]);
428530
}

extensions/positron-assistant/src/tools/notebookUtils.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,76 @@ export function validateCellIndices(
7070
return { valid: true };
7171
}
7272

73+
/**
74+
* Validation result for permutation arrays
75+
*/
76+
export interface PermutationValidation {
77+
valid: boolean;
78+
error?: string;
79+
isIdentity?: boolean;
80+
}
81+
82+
/**
83+
* Validates a permutation array for reordering cells.
84+
* A valid permutation must contain each index from 0 to cellCount-1 exactly once.
85+
*
86+
* @param newOrder The proposed new order array
87+
* @param cellCount The total number of cells in the notebook
88+
* @returns Validation result with error message if invalid, and whether it's an identity permutation
89+
*/
90+
export function validatePermutation(
91+
newOrder: number[],
92+
cellCount: number
93+
): PermutationValidation {
94+
// Check length matches
95+
if (newOrder.length !== cellCount) {
96+
return {
97+
valid: false,
98+
error: `Permutation length (${newOrder.length}) must match cell count (${cellCount})`
99+
};
100+
}
101+
102+
// Handle empty notebook case
103+
if (cellCount === 0) {
104+
return { valid: true, isIdentity: true };
105+
}
106+
107+
// An identity permutation is just one where each index maps to itself. Aka a no-op.
108+
let isIdentity = true;
109+
110+
// Check if any indices are outside of the valid range
111+
for (const [i, index] of newOrder.entries()) {
112+
if (!Number.isInteger(index) || index < 0 || index >= cellCount) {
113+
return {
114+
valid: false,
115+
error: `Invalid index in permutation: ${index} (must be integer between 0 and ${cellCount - 1})`
116+
};
117+
}
118+
119+
// Check for identity at the same time
120+
if (index !== i) {
121+
isIdentity = false;
122+
}
123+
}
124+
125+
// If identity permutation, no need to check further
126+
if (isIdentity) {
127+
return { valid: true, isIdentity: true };
128+
}
129+
130+
// Make sure there are no duplicates and all indices are present
131+
const uniqueIndices = new Set(newOrder);
132+
133+
if (uniqueIndices.size !== cellCount) {
134+
return {
135+
valid: false,
136+
error: 'Invalid permutation: must contain each index from 0 to cellCount-1 exactly once'
137+
};
138+
}
139+
140+
return { valid: true, isIdentity: false };
141+
}
142+
73143
/**
74144
* Fetches and formats cell content for preview in confirmation dialogs.
75145
* Truncates long content with ellipsis.

src/positron-dts/positron.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,5 +2575,22 @@ declare module 'positron' {
25752575
* @returns Array of output objects with MIME type and data
25762576
*/
25772577
export function getCellOutputs(notebookUri: string, cellIndex: number): Thenable<NotebookCellOutput[]>;
2578+
2579+
/**
2580+
* Move a cell from one index to another in a notebook
2581+
* @param notebookUri URI of the notebook
2582+
* @param fromIndex The current index of the cell to move
2583+
* @param toIndex The target index where the cell should be moved to
2584+
*/
2585+
export function moveCell(notebookUri: string, fromIndex: number, toIndex: number): Thenable<void>;
2586+
2587+
/**
2588+
* Reorder all cells in a notebook according to a new order
2589+
* @param notebookUri URI of the notebook
2590+
* @param newOrder Array representing the new order - newOrder[i] is the index of the cell
2591+
* that should be at position i in the reordered notebook.
2592+
* Must be a valid permutation containing each index from 0 to cellCount-1 exactly once.
2593+
*/
2594+
export function reorderCells(notebookUri: string, newOrder: number[]): Thenable<void>;
25782595
}
25792596
}

0 commit comments

Comments
 (0)