Преглед изворни кода

feat: Jupyter Notebook Enhancements (#8053)

* Jupyter Notebook Enhancements

* fix: implement dynamic notebook instructions for replace_in_file

Leverages the new dynamic prompt infrastructure to conditionally inject Jupyter Notebook-specific instructions into the `replace_in_file` tool. This ensures that the model receives guidance on handling JSON structure in `.ipynb` files only when the `enhancedNotebookInteractionEnabled` setting is active, keeping the default prompt clean for other users.

- Added `enhancedNotebookInteractionEnabled` to global settings and system prompt context
- Updated `replace_in_file` tool to use a dynamic instruction function that appends notebook rules based on context
- Wired up state management to pass the setting value to the prompt builder

* refactor: unify notebook output sanitization across two code paths

Previously, context menu commands (Add to Cline, Explain, etc.) wiped ALL
notebook outputs, while file mentions preserved text and only truncated images.
Additionally, when outputs weren't cleared, massive base64-encoded image data
was sent directly to the LLM, flooding context with garbage.

Changes:
- Create shared notebook-utils.ts with sanitization logic
- Update extract-text.ts to use shared utility
- Update commandUtils.ts to sanitize instead of clearing outputs

Both paths now truncate base64 image data with "[IMAGE DATA TRUNCATED]"
while preserving useful text outputs like print statements and errors.

* feat(improve): unify prompt sending behavior for improveWithCline

Refactor improveWithCline to build a single prompt and handle sending uniformly: for notebooks, populate existing task if available and send immediately; otherwise, create new task. This unifies behavior across selected text and notebook contexts, removing dependency on sendAddToInputEvent and simplifying logic. Minor formatting tweaks in extension.ts for notebook context string.

* refactor: remove notebook_cell_json from proto, move notebook context to dedicated commands

The contributor's original implementation added notebook_cell_json to the
CommandContext proto, which was then populated in getContextForCommand() for
any notebook file when enhancedNotebookInteractionEnabled was set.

This couples notebook-specific functionality to the general command proto,
which feels heavy for a niche feature. Protos should stay clean and general.

Changes:
- Remove notebook_cell_json field from CommandContext proto
- Export findMatchingNotebookCell() from commandUtils.ts
- Update Jupyter commands (JupyterGenerateCell, JupyterExplainCell,
  JupyterImproveCell) in extension.ts to fetch cell JSON directly and
  bundle it into the notebookContext parameter
- Update command files to use only notebookContext parameter
- Remove notebook-specific handling from getContextForCommand()

Result: Notebook context only flows through dedicated Jupyter commands.
Regular commands (Add to Cline, Fix, etc.) work the same for all file types.
The proto stays clean and general-purpose.

Note: This removes the behavior where regular commands would get notebook
context when used on .ipynb files with enhancedNotebookInteractionEnabled.
That feature is now exclusive to the dedicated Jupyter menu commands.

* feat: improve notebook handling for empty notebooks

- Add semicolon to import statement for consistency
- Prevent errors by checking cell count before accessing notebook cells
- Add fallback in getContextForCommand for active notebook editor when no text editor is available
- Ensures robustness when dealing with empty or cell-less notebooks in the VSCode extension

* refactor: extract common notebook context logic for Jupyter commands

Extracted duplicated code into a helper function `getNotebookCommandContext` to handle active notebook checks, context retrieval, and cell JSON fetching. This reduces duplication in `JupyterGenerateCell` and `JupyterExplainCell` commands, improving code maintainability and readability. Minor import semicolon fix for consistency.

* fix: block notebook edits when enhanced interaction disabled

Prevent crashes when enhancedNotebookInteractionEnabled is false by blocking .ipynb file edits in WriteToFileToolHandler. Added validation to return an error message instructing the user to enable the setting, and set didRejectTool to stop the operation. Reading notebooks remains unaffected.

* fix(mentions): reorder parameters in parseMentions signature

Reordered the parameters in the parseMentions function to move the default parameter to the end of the argument list. This change ensures consistency in the function signature and correctly aligns arguments at the call site in the Task class.

This update was done to fix failing tests.

* Created proper diff views for vscode nd removed unnecessary logs

* feat: make replace_in_file prompt dynamic based on open files

Add editorTabs to SystemPromptContext to expose open/visible files.

Populate editorTabs in Task using HostProvider.

Conditionally include notebook-specific instructions in replace_in_file tool only when .ipynb files are open or visible.

Refactor replace_in_file prompt construction for better readability.

* feat: enable enhanced notebook interaction by default

Remove enhancedNotebookInteractionEnabled feature flag and enable notebook support globally.

Update tool handlers to process notebook cells automatically.

Update file extraction logic to support .ipynb files natively.

Clean up settings UI and state management.

* fix: restore accidentally removed promptContext fields

Commit ec68e7c2a accidentally removed enableParallelToolCalling and
terminalExecutionMode from promptContext when refactoring to add
editorTabs. These fields are still in SystemPromptContext interface
and actively used by system prompt templates.

* fix: complete feature flag removal from package.json

Commit a1dc73f93 removed the enhancedNotebookInteractionEnabled flag
from runtime code but forgot to update package.json. The Jupyter menu
items were hidden because the when conditions checked a setting that
defaulted to false.

- Remove config check from notebook menu item when conditions
- Remove unused setting definition

* fix: change changeset from minor to patch

* fix: code quality improvements in VscodeDiffViewProvider

- Use proper ES6 import for os module instead of require()
- Remove dead commented-out code (closeCurrentTextDiffEditor)
- Improve comment explaining the render delay

* fix: watch specific temp file instead of entire directory

* test: update snapshots for replace_in_file whitespace change

The PR's refactoring of replace_in_file.ts changed indentation in
the tool description from tabs to spaces. Updating snapshots to
match.

* fix: remove merge artifact marginTop from checkpoints div

* test: update DiffViewProvider test stub for new abstract method

* fix: remove dead notebook diff view code (switchToSpecializedEditor)

The switchToSpecializedEditor() method was declared as abstract and
implemented in all DiffViewProvider subclasses, but was never called
from anywhere. This meant ~180 lines of notebook diff view code
(temp file management, file watchers, cleanup) would never execute.

Removing this dead code. The notebook diff view feature will need a
follow-up PR to properly integrate it by calling the method from the
update() flow when isFinal is true.

---------

Co-authored-by: Robin Newhouse <[email protected]>
Co-authored-by: Saoud Rizwan <[email protected]>
tekulam пре 1 дан
родитељ
комит
d8aefbaabd

+ 5 - 0
.changeset/ready-doodles-try.md

@@ -0,0 +1,5 @@
+---
+"claude-dev": patch
+---
+
+This pull request introduces comprehensive Jupyter Notebook support for Cline, enabling AI-assisted editing of `.ipynb` files with full cell-level context awareness. The feature allows users to seamlessly work with Jupyter notebooks using Cline's AI capabilities while preserving the notebook's JSON structure.

+ 37 - 0
package.json

@@ -201,6 +201,24 @@
 				"title": "Improve with Cline",
 				"title": "Improve with Cline",
 				"category": "Cline"
 				"category": "Cline"
 			},
 			},
+			{
+				"command": "cline.jupyterGenerateCell",
+				"title": "Generate Jupyter Cell with Cline",
+				"category": "Cline",
+				"icon": "$(sparkle)"
+			},
+			{
+				"command": "cline.jupyterExplainCell",
+				"title": "Explain Jupyter Cell with Cline",
+				"category": "Cline",
+				"icon": "$(question)"
+			},
+			{
+				"command": "cline.jupyterImproveCell",
+				"title": "Improve Jupyter Cell with Cline",
+				"category": "Cline",
+				"icon": "$(lightbulb)"
+			},
 			{
 			{
 				"command": "cline.openWalkthrough",
 				"command": "cline.openWalkthrough",
 				"title": "Open Walkthrough",
 				"title": "Open Walkthrough",
@@ -304,6 +322,25 @@
 					"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
 					"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
 				}
 				}
 			],
 			],
+			"notebook/toolbar": [
+				{
+					"command": "cline.jupyterGenerateCell",
+					"group": "navigation/add@1",
+					"when": "notebookType == 'jupyter-notebook'"
+				}
+			],
+			"notebook/cell/title": [
+				{
+					"command": "cline.jupyterExplainCell",
+					"group": "inline@1",
+					"when": "notebookType == 'jupyter-notebook'"
+				},
+				{
+					"command": "cline.jupyterImproveCell",
+					"group": "inline@2",
+					"when": "notebookType == 'jupyter-notebook'"
+				}
+			],
 			"commandPalette": [
 			"commandPalette": [
 				{
 				{
 					"command": "cline.generateGitCommitMessage",
 					"command": "cline.generateGitCommitMessage",

+ 20 - 4
src/core/controller/commands/addToCline.ts

@@ -1,5 +1,6 @@
 import { getFileMentionFromPath } from "@/core/mentions"
 import { getFileMentionFromPath } from "@/core/mentions"
 import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
 import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
+import { Logger } from "@/services/logging/Logger"
 import { telemetryService } from "@/services/telemetry"
 import { telemetryService } from "@/services/telemetry"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { Controller } from "../index"
 import { Controller } from "../index"
@@ -7,8 +8,9 @@ import { sendAddToInputEvent } from "../ui/subscribeToAddToInput"
 
 
 // 'Add to Cline' context menu in editor and code action
 // 'Add to Cline' context menu in editor and code action
 // Inserts the selected code into the chat.
 // Inserts the selected code into the chat.
-export async function addToCline(controller: Controller, request: CommandContext): Promise<Empty> {
-	if (!request.selectedText) {
+export async function addToCline(controller: Controller, request: CommandContext, notebookContext?: string): Promise<Empty> {
+	if (!request.selectedText?.trim() && !notebookContext) {
+		Logger.log("❌ No text selected and no notebook context - returning early")
 		return {}
 		return {}
 	}
 	}
 
 
@@ -16,14 +18,28 @@ export async function addToCline(controller: Controller, request: CommandContext
 	const fileMention = await getFileMentionFromPath(filePath)
 	const fileMention = await getFileMentionFromPath(filePath)
 
 
 	let input = `${fileMention}\n\`\`\`\n${request.selectedText}\n\`\`\``
 	let input = `${fileMention}\n\`\`\`\n${request.selectedText}\n\`\`\``
+
+	// Add notebook context if provided (includes cell JSON)
+	if (notebookContext) {
+		Logger.log("Adding notebook context for enhanced editing")
+		input += `\n${notebookContext}`
+	}
+
 	if (request.diagnostics.length) {
 	if (request.diagnostics.length) {
 		const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
 		const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
 		input += `\nProblems:\n${problemsString}`
 		input += `\nProblems:\n${problemsString}`
 	}
 	}
 
 
-	await sendAddToInputEvent(input)
+	// Notebooks send immediately, regular adds just fill input
+	if (notebookContext && controller.task) {
+		await controller.task.handleWebviewAskResponse("messageResponse", input)
+	} else if (notebookContext) {
+		await controller.initTask(input)
+	} else {
+		await sendAddToInputEvent(input)
+	}
 
 
-	console.log("addToCline", request.selectedText, filePath, request.language)
+	console.log("addToCline", request.selectedText, filePath, request.language, notebookContext ? "with notebook context" : "")
 	telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
 	telemetryService.captureButtonClick("codeAction_addToChat", controller.task?.ulid)
 
 
 	return {}
 	return {}

+ 18 - 4
src/core/controller/commands/explainWithCline.ts

@@ -1,21 +1,35 @@
 import { getFileMentionFromPath } from "@/core/mentions"
 import { getFileMentionFromPath } from "@/core/mentions"
 import { HostProvider } from "@/hosts/host-provider"
 import { HostProvider } from "@/hosts/host-provider"
+import { Logger } from "@/services/logging/Logger"
 import { telemetryService } from "@/services/telemetry"
 import { telemetryService } from "@/services/telemetry"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { ShowMessageType } from "@/shared/proto/index.host"
 import { ShowMessageType } from "@/shared/proto/index.host"
 import { Controller } from "../index"
 import { Controller } from "../index"
 
 
-export async function explainWithCline(controller: Controller, request: CommandContext): Promise<Empty> {
-	if (!request.selectedText || !request.selectedText.trim()) {
+export async function explainWithCline(
+	controller: Controller,
+	request: CommandContext,
+	notebookContext?: string,
+): Promise<Empty> {
+	if (!request.selectedText?.trim() && !notebookContext) {
 		HostProvider.window.showMessage({
 		HostProvider.window.showMessage({
 			type: ShowMessageType.INFORMATION,
 			type: ShowMessageType.INFORMATION,
 			message: "Please select some code to explain.",
 			message: "Please select some code to explain.",
 		})
 		})
 		return {}
 		return {}
 	}
 	}
-	const fileMention = await getFileMentionFromPath(request.filePath || "")
-	const prompt = `Explain the following code from ${fileMention}:
+
+	const filePath = request.filePath || ""
+	const fileMention = await getFileMentionFromPath(filePath)
+	let prompt = `Explain the following code from ${fileMention}:
 \`\`\`${request.language}\n${request.selectedText}\n\`\`\``
 \`\`\`${request.language}\n${request.selectedText}\n\`\`\``
+
+	// Add notebook context if provided (includes cell JSON)
+	if (notebookContext) {
+		Logger.log("Adding notebook context to explainWithCline task")
+		prompt += notebookContext
+	}
+
 	await controller.initTask(prompt)
 	await controller.initTask(prompt)
 	telemetryService.captureButtonClick("codeAction_explainCode", controller.task?.ulid)
 	telemetryService.captureButtonClick("codeAction_explainCode", controller.task?.ulid)
 
 

+ 4 - 4
src/core/controller/commands/fixWithCline.ts

@@ -9,10 +9,10 @@ export async function fixWithCline(controller: Controller, request: CommandConte
 	const fileMention = await getFileMentionFromPath(filePath)
 	const fileMention = await getFileMentionFromPath(filePath)
 	const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
 	const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
 
 
-	await controller.initTask(
-		`Fix the following code in ${fileMention}
-\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`,
-	)
+	const taskMessage = `Fix the following code in ${fileMention}
+\`\`\`\n${request.selectedText}\n\`\`\`\n\nProblems:\n${problemsString}`
+
+	await controller.initTask(taskMessage)
 	console.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
 	console.log("fixWithCline", request.selectedText, request.filePath, request.language, problemsString)
 
 
 	telemetryService.captureButtonClick("codeAction_fixWithCline", controller.task?.ulid)
 	telemetryService.captureButtonClick("codeAction_fixWithCline", controller.task?.ulid)

+ 27 - 6
src/core/controller/commands/improveWithCline.ts

@@ -1,23 +1,44 @@
 import { getFileMentionFromPath } from "@/core/mentions"
 import { getFileMentionFromPath } from "@/core/mentions"
 import { HostProvider } from "@/hosts/host-provider"
 import { HostProvider } from "@/hosts/host-provider"
+import { Logger } from "@/services/logging/Logger"
 import { telemetryService } from "@/services/telemetry"
 import { telemetryService } from "@/services/telemetry"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { CommandContext, Empty } from "@/shared/proto/index.cline"
 import { ShowMessageType } from "@/shared/proto/index.host"
 import { ShowMessageType } from "@/shared/proto/index.host"
 import { Controller } from "../index"
 import { Controller } from "../index"
 
 
-export async function improveWithCline(controller: Controller, request: CommandContext): Promise<Empty> {
-	if (!request.selectedText || !request.selectedText.trim()) {
+export async function improveWithCline(
+	controller: Controller,
+	request: CommandContext,
+	notebookContext?: string,
+): Promise<Empty> {
+	if (!request.selectedText?.trim() && !notebookContext) {
+		Logger.log("❌ No text selected and no notebook context")
 		HostProvider.window.showMessage({
 		HostProvider.window.showMessage({
 			type: ShowMessageType.INFORMATION,
 			type: ShowMessageType.INFORMATION,
 			message: "Please select some code to improve.",
 			message: "Please select some code to improve.",
 		})
 		})
 		return {}
 		return {}
 	}
 	}
-	const fileMention = await getFileMentionFromPath(request.filePath || "")
-	const prompt = `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):
-\`\`\`${request.language}\n${request.selectedText}\n\`\`\``
+	const filePath = request.filePath || ""
+	const fileMention = await getFileMentionFromPath(filePath)
+	const hasSelectedText = request.selectedText?.trim()
 
 
-	await controller.initTask(prompt)
+	// Build prompt
+	let prompt = hasSelectedText
+		? `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):\n\`\`\`${request.language}\n${request.selectedText}\n\`\`\``
+		: `Improve the current code in the current notebook cell from ${fileMention}. Suggest refactorings, optimizations, or better practices based on the cell context.`
+
+	if (notebookContext) {
+		Logger.log("Adding notebook context to improveWithCline task")
+		prompt += `\n${notebookContext}`
+	}
+
+	// Send: notebooks go to existing task if available, non-notebooks always create new task
+	if (notebookContext && controller.task) {
+		await controller.task.handleWebviewAskResponse("messageResponse", prompt)
+	} else {
+		await controller.initTask(prompt)
+	}
 
 
 	telemetryService.captureButtonClick("codeAction_improveCode", controller.task?.ulid)
 	telemetryService.captureButtonClick("codeAction_improveCode", controller.task?.ulid)
 
 

+ 0 - 1
src/core/controller/state/updateSettings.ts

@@ -1,5 +1,4 @@
 import { buildApiHandler } from "@core/api"
 import { buildApiHandler } from "@core/api"
-
 import { Empty } from "@shared/proto/cline/common"
 import { Empty } from "@shared/proto/cline/common"
 import {
 import {
 	PlanActMode,
 	PlanActMode,

+ 1 - 1
src/core/prompts/system-prompt/__tests__/__snapshots__/cline_native_next_gen.tools.snap

@@ -125,7 +125,7 @@
           },
           },
           "diff": {
           "diff": {
             "type": "string",
             "type": "string",
-            "description": "One or more SEARCH/REPLACE blocks following this exact format:\n  ```\n  ------- SEARCH\n  [exact content to find]\n  =======\n  [new content to replace with]\n  +++++++ REPLACE\n  ```\n  Critical rules:\n  1. SEARCH content must match the associated file section to find EXACTLY:\n\t * Match character-for-character including whitespace, indentation, line endings\n\t * Include all comments, docstrings, etc.\n  2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.\n\t * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.\n\t * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.\n\t * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.\n  3. Keep SEARCH/REPLACE blocks concise:\n\t * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.\n\t * Include just the changing lines, and a few surrounding lines if needed for uniqueness.\n\t * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.\n\t * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.\n  4. Special operations:\n\t * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)\n\t * To delete code: Use empty REPLACE section"
+            "description": "One or more SEARCH/REPLACE blocks following this exact format:\n  ```\n  ------- SEARCH\n  [exact content to find]\n  =======\n  [new content to replace with]\n  +++++++ REPLACE\n  ```\n  Critical rules:\n  1. SEARCH content must match the associated file section to find EXACTLY:\n     * Match character-for-character including whitespace, indentation, line endings\n     * Include all comments, docstrings, etc.\n  2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.\n     * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.\n     * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.\n     * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.\n  3. Keep SEARCH/REPLACE blocks concise:\n     * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.\n     * Include just the changing lines, and a few surrounding lines if needed for uniqueness.\n     * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.\n     * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.\n  4. Special operations:\n     * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)\n     * To delete code: Use empty REPLACE section"
           },
           },
           "task_progress": {
           "task_progress": {
             "type": "string",
             "type": "string",

+ 52 - 43
src/core/prompts/system-prompt/tools/replace_in_file.ts

@@ -1,27 +1,19 @@
 import { ModelFamily } from "@/shared/prompts"
 import { ModelFamily } from "@/shared/prompts"
 import { ClineDefaultTool } from "@/shared/tools"
 import { ClineDefaultTool } from "@/shared/tools"
 import type { ClineToolSpec } from "../spec"
 import type { ClineToolSpec } from "../spec"
-import { TASK_PROGRESS_PARAMETER } from "../types"
+import { SystemPromptContext, TASK_PROGRESS_PARAMETER } from "../types"
 
 
 const id = ClineDefaultTool.FILE_EDIT
 const id = ClineDefaultTool.FILE_EDIT
 
 
-const generic: ClineToolSpec = {
-	variant: ModelFamily.GENERIC,
-	id,
-	name: "replace_in_file",
-	description:
-		"Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.",
-	parameters: [
-		{
-			name: "path",
-			required: true,
-			instruction: `The path of the file to modify (relative to the current working directory {{CWD}})`,
-			usage: "File path here",
-		},
-		{
-			name: "diff",
-			required: true,
-			instruction: `One or more SEARCH/REPLACE blocks following this exact format:
+const getOpenOrVisibleTabPaths = (context: SystemPromptContext) => {
+	return [...(context.editorTabs?.open ?? []), ...(context.editorTabs?.visible ?? [])]
+}
+
+const shouldIncludeNotebookInstructions = (context: SystemPromptContext) => {
+	return getOpenOrVisibleTabPaths(context).some((p) => p.endsWith(".ipynb"))
+}
+
+const BASE_DIFF_INSTRUCTIONS = `One or more SEARCH/REPLACE blocks following this exact format:
   \`\`\`
   \`\`\`
   ------- SEARCH
   ------- SEARCH
   [exact content to find]
   [exact content to find]
@@ -44,7 +36,47 @@ const generic: ClineToolSpec = {
      * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.
      * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.
   4. Special operations:
   4. Special operations:
      * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)
      * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)
-     * To delete code: Use empty REPLACE section`,
+     * To delete code: Use empty REPLACE section`
+
+const NOTEBOOK_INSTRUCTIONS = `
+  5. For Jupyter Notebook (.ipynb) files:
+     * Match the exact JSON structure including quotes, commas, and \\n characters
+     * Each line in "source" array (except last) must end with "\\n"
+     * Each source line is a separate JSON string in the array
+     * Example SEARCH block for notebook:
+       ------- SEARCH
+         "source": [
+           "x = 10\\n",
+           "print(x)"
+         ]
+       =======
+         "source": [
+           "x = 100\\n",
+           "print(x)"
+         ]
+       +++++++ REPLACE`
+
+const diffInstruction = (context: SystemPromptContext) => {
+	return shouldIncludeNotebookInstructions(context) ? BASE_DIFF_INSTRUCTIONS + NOTEBOOK_INSTRUCTIONS : BASE_DIFF_INSTRUCTIONS
+}
+
+const generic: ClineToolSpec = {
+	variant: ModelFamily.GENERIC,
+	id,
+	name: "replace_in_file",
+	description:
+		"Request to replace sections of content in an existing file using SEARCH/REPLACE blocks that define exact changes to specific parts of the file. This tool should be used when you need to make targeted changes to specific parts of a file.",
+	parameters: [
+		{
+			name: "path",
+			required: true,
+			instruction: `The path of the file to modify (relative to the current working directory {{CWD}})`,
+			usage: "File path here",
+		},
+		{
+			name: "diff",
+			required: true,
+			instruction: diffInstruction,
 			usage: "Search and replace blocks here",
 			usage: "Search and replace blocks here",
 		},
 		},
 		TASK_PROGRESS_PARAMETER,
 		TASK_PROGRESS_PARAMETER,
@@ -66,30 +98,7 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
 		{
 		{
 			name: "diff",
 			name: "diff",
 			required: true,
 			required: true,
-			instruction: `One or more SEARCH/REPLACE blocks following this exact format:
-  \`\`\`
-  ------- SEARCH
-  [exact content to find]
-  =======
-  [new content to replace with]
-  +++++++ REPLACE
-  \`\`\`
-  Critical rules:
-  1. SEARCH content must match the associated file section to find EXACTLY:
-	 * Match character-for-character including whitespace, indentation, line endings
-	 * Include all comments, docstrings, etc.
-  2. SEARCH/REPLACE blocks will ONLY replace the first match occurrence.
-	 * Including multiple unique SEARCH/REPLACE blocks if you need to make multiple changes.
-	 * Include *just* enough lines in each SEARCH section to uniquely match each set of lines that need to change.
-	 * When using multiple SEARCH/REPLACE blocks, list them in the order they appear in the file.
-  3. Keep SEARCH/REPLACE blocks concise:
-	 * Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file.
-	 * Include just the changing lines, and a few surrounding lines if needed for uniqueness.
-	 * Do not include long runs of unchanging lines in SEARCH/REPLACE blocks.
-	 * Each line must be complete. Never truncate lines mid-way through as this can cause matching failures.
-  4. Special operations:
-	 * To move code: Use two SEARCH/REPLACE blocks (one to delete from original + one to insert at new location)
-	 * To delete code: Use empty REPLACE section`,
+			instruction: diffInstruction,
 		},
 		},
 		TASK_PROGRESS_PARAMETER,
 		TASK_PROGRESS_PARAMETER,
 	],
 	],

+ 4 - 0
src/core/prompts/system-prompt/types.ts

@@ -95,6 +95,10 @@ export interface SystemPromptContext {
 	readonly providerInfo: ApiProviderInfo
 	readonly providerInfo: ApiProviderInfo
 	readonly cwd?: string
 	readonly cwd?: string
 	readonly ide: string
 	readonly ide: string
+	readonly editorTabs?: {
+		readonly open?: readonly string[]
+		readonly visible?: readonly string[]
+	}
 	readonly supportsBrowserUse?: boolean
 	readonly supportsBrowserUse?: boolean
 	readonly mcpHub?: McpHub
 	readonly mcpHub?: McpHub
 	readonly skills?: SkillMetadata[]
 	readonly skills?: SkillMetadata[]

+ 11 - 0
src/core/task/index.ts

@@ -1779,10 +1779,21 @@ export class Task {
 			return toggles[skill.path] !== false
 			return toggles[skill.path] !== false
 		})
 		})
 
 
+		// Snapshot editor tabs so prompt tools can decide whether to include
+		// filetype-specific instructions (e.g. notebooks) without adding bespoke flags.
+		const openTabPaths = (await HostProvider.window.getOpenTabs({})).paths || []
+		const visibleTabPaths = (await HostProvider.window.getVisibleTabs({})).paths || []
+		const cap = 50
+		const editorTabs = {
+			open: openTabPaths.slice(0, cap),
+			visible: visibleTabPaths.slice(0, cap),
+		}
+
 		const promptContext: SystemPromptContext = {
 		const promptContext: SystemPromptContext = {
 			cwd: this.cwd,
 			cwd: this.cwd,
 			ide,
 			ide,
 			providerInfo,
 			providerInfo,
+			editorTabs,
 			supportsBrowserUse,
 			supportsBrowserUse,
 			mcpHub: this.mcpHub,
 			mcpHub: this.mcpHub,
 			skills: availableSkills,
 			skills: availableSkills,

+ 148 - 1
src/extension.ts

@@ -32,7 +32,7 @@ import { sendShowWebviewEvent } from "./core/controller/ui/subscribeToShowWebvie
 import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
 import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
 import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
 import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
 import { workspaceResolver } from "./core/workspace"
 import { workspaceResolver } from "./core/workspace"
-import { getContextForCommand, showWebview } from "./hosts/vscode/commandUtils"
+import { findMatchingNotebookCell, getContextForCommand, showWebview } from "./hosts/vscode/commandUtils"
 import { abortCommitGeneration, generateCommitMsg } from "./hosts/vscode/commit-message-generator"
 import { abortCommitGeneration, generateCommitMsg } from "./hosts/vscode/commit-message-generator"
 import {
 import {
 	disposeVscodeCommentReviewController,
 	disposeVscodeCommentReviewController,
@@ -395,6 +395,107 @@ export async function activate(context: vscode.ExtensionContext) {
 		}),
 		}),
 	)
 	)
 
 
+	// Register Jupyter Notebook command handlers
+	const NOTEBOOK_EDIT_INSTRUCTIONS = `Special considerations for using replace_in_file on *.ipynb files:
+* Jupyter notebook files are JSON format with specific structure for source code cells
+* Source code in cells is stored as JSON string arrays ending with explicit \\n characters and commas
+* Always match the exact JSON format including quotes, commas, and escaped newlines.`
+
+	// Helper to get notebook context for Jupyter commands
+	async function getNotebookCommandContext(range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) {
+		const activeNotebook = vscode.window.activeNotebookEditor
+		if (!activeNotebook) {
+			HostProvider.window.showMessage({
+				type: ShowMessageType.ERROR,
+				message: "No active Jupyter notebook found. Please open a .ipynb file first.",
+			})
+			return null
+		}
+
+		const ctx = await getContextForCommand(range, diagnostics)
+		if (!ctx) {
+			return null
+		}
+
+		const filePath = ctx.commandContext.filePath || ""
+		let cellJson: string | null = null
+		if (activeNotebook.notebook.cellCount > 0) {
+			const cellIndex = activeNotebook.notebook.cellAt(activeNotebook.selection.start).index
+			cellJson = await findMatchingNotebookCell(filePath, cellIndex)
+		}
+
+		return { ...ctx, cellJson }
+	}
+
+	context.subscriptions.push(
+		vscode.commands.registerCommand(
+			commands.JupyterGenerateCell,
+			async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
+				const userPrompt = await showJupyterPromptInput(
+					"Generate Notebook Cell",
+					"Enter your prompt for generating notebook cell (press Enter to confirm & Esc to cancel)",
+				)
+				if (!userPrompt) return
+
+				const ctx = await getNotebookCommandContext(range, diagnostics)
+				if (!ctx) return
+
+				const notebookContext = `User prompt: ${userPrompt}
+Insert a new Jupyter notebook cell above or below the current cell based on user prompt.
+${NOTEBOOK_EDIT_INSTRUCTIONS}
+
+Current Notebook Cell Context (JSON, sanitized of image data):
+\`\`\`json
+${ctx.cellJson || "{}"}
+\`\`\``
+
+				await addToCline(ctx.controller, ctx.commandContext, notebookContext)
+			},
+		),
+	)
+
+	context.subscriptions.push(
+		vscode.commands.registerCommand(
+			commands.JupyterExplainCell,
+			async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
+				const ctx = await getNotebookCommandContext(range, diagnostics)
+				if (!ctx) return
+
+				const notebookContext = ctx.cellJson
+					? `\n\nCurrent Notebook Cell Context (JSON, sanitized of image data):\n\`\`\`json\n${ctx.cellJson}\n\`\`\``
+					: undefined
+
+				await explainWithCline(ctx.controller, ctx.commandContext, notebookContext)
+			},
+		),
+	)
+
+	context.subscriptions.push(
+		vscode.commands.registerCommand(
+			commands.JupyterImproveCell,
+			async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
+				const userPrompt = await showJupyterPromptInput(
+					"Improve Notebook Cell",
+					"Enter your prompt for improving the current notebook cell (press Enter to confirm & Esc to cancel)",
+				)
+				if (!userPrompt) return
+
+				const ctx = await getNotebookCommandContext(range, diagnostics)
+				if (!ctx) return
+
+				const notebookContext = `User prompt: ${userPrompt}
+${NOTEBOOK_EDIT_INSTRUCTIONS}
+
+Current Notebook Cell Context (JSON, sanitized of image data):
+\`\`\`json
+${ctx.cellJson || "{}"}
+\`\`\``
+
+				await improveWithCline(ctx.controller, ctx.commandContext, notebookContext)
+			},
+		),
+	)
+
 	// Register the openWalkthrough command handler
 	// Register the openWalkthrough command handler
 	context.subscriptions.push(
 	context.subscriptions.push(
 		vscode.commands.registerCommand(commands.Walkthrough, async () => {
 		vscode.commands.registerCommand(commands.Walkthrough, async () => {
@@ -445,6 +546,52 @@ export async function activate(context: vscode.ExtensionContext) {
 	return createClineAPI(webview.controller)
 	return createClineAPI(webview.controller)
 }
 }
 
 
+async function showJupyterPromptInput(title: string, placeholder: string): Promise<string | undefined> {
+	return new Promise((resolve) => {
+		const quickPick = vscode.window.createQuickPick()
+		quickPick.title = title
+		quickPick.placeholder = placeholder
+		quickPick.ignoreFocusOut = true
+
+		// Allow free text input
+		quickPick.canSelectMany = false
+
+		let userInput = ""
+
+		quickPick.onDidChangeValue((value) => {
+			userInput = value
+			// Update items to show the current input
+			if (value) {
+				quickPick.items = [
+					{
+						label: "$(check) Use this prompt",
+						detail: value,
+						alwaysShow: true,
+					},
+				]
+			} else {
+				quickPick.items = []
+			}
+		})
+
+		quickPick.onDidAccept(() => {
+			if (userInput) {
+				resolve(userInput)
+				quickPick.hide()
+			}
+		})
+
+		quickPick.onDidHide(() => {
+			if (!userInput) {
+				resolve(undefined)
+			}
+			quickPick.dispose()
+		})
+
+		quickPick.show()
+	})
+}
+
 function setupHostProvider(context: ExtensionContext) {
 function setupHostProvider(context: ExtensionContext) {
 	console.log("Setting up vscode host providers...")
 	console.log("Setting up vscode host providers...")
 
 

+ 1 - 0
src/hosts/vscode/VscodeDiffViewProvider.ts

@@ -96,6 +96,7 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
 		if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
 		if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
 			throw new Error("User closed text editor, unable to edit file...")
 			throw new Error("User closed text editor, unable to edit file...")
 		}
 		}
+
 		// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
 		// Place cursor at the beginning of the diff editor to keep it out of the way of the stream animation
 		const beginningOfDocument = new vscode.Position(0, 0)
 		const beginningOfDocument = new vscode.Position(0, 0)
 		this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
 		this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)

+ 48 - 1
src/hosts/vscode/commandUtils.ts

@@ -1,10 +1,49 @@
+import * as fs from "fs/promises"
 import * as vscode from "vscode"
 import * as vscode from "vscode"
+import { sanitizeCellForLLM } from "@/integrations/misc/notebook-utils"
 import { ExtensionRegistryInfo } from "@/registry"
 import { ExtensionRegistryInfo } from "@/registry"
+import { Logger } from "@/services/logging/Logger"
 import { CommandContext } from "@/shared/proto/index.cline"
 import { CommandContext } from "@/shared/proto/index.cline"
 import { Controller } from "../../core/controller"
 import { Controller } from "../../core/controller"
 import { WebviewProvider } from "../../core/webview"
 import { WebviewProvider } from "../../core/webview"
 import { convertVscodeDiagnostics } from "./hostbridge/workspace/getDiagnostics"
 import { convertVscodeDiagnostics } from "./hostbridge/workspace/getDiagnostics"
 
 
+/**
+ * Finds the notebook cell that contains the selected text and returns its JSON representation
+ * @param filePath Path to the .ipynb file
+ * @param notebookCell The cell index from the active notebook editor
+ * @returns JSON string of the matching cell, or null if no match found
+ */
+export async function findMatchingNotebookCell(filePath: string, notebookCell?: number): Promise<string | null> {
+	try {
+		// Read the notebook file directly
+		const notebookContent = await fs.readFile(filePath, "utf8")
+		const notebook = JSON.parse(notebookContent)
+
+		if (!notebook.cells || !Array.isArray(notebook.cells)) {
+			Logger.log("Invalid notebook structure: no cells array found")
+			return null
+		}
+
+		Logger.log(`Loaded notebook with ${notebook.cells.length} cells`)
+
+		if (typeof notebookCell === "number" && notebookCell >= 0 && notebookCell < notebook.cells.length) {
+			Logger.log(`Using provided notebook cell number ${notebookCell}`)
+			// Get a reference to the specific cell object
+			const cellToProcess = notebook.cells[notebookCell]
+
+			// Sanitize the cell outputs (truncate images, keep text outputs)
+			return sanitizeCellForLLM(cellToProcess)
+		}
+
+		Logger.log("No valid notebook cell number provided")
+		return null
+	} catch (error) {
+		Logger.error("Error in findMatchingNotebookCell:", error)
+		return null
+	}
+}
+
 /**
 /**
  * Gets the context needed for VSCode commands that interact with the editor
  * Gets the context needed for VSCode commands that interact with the editor
  * @param range Optional range to use instead of current selection
  * @param range Optional range to use instead of current selection
@@ -34,7 +73,14 @@ export async function getContextForCommand(
 
 
 	const editor = vscode.window.activeTextEditor
 	const editor = vscode.window.activeTextEditor
 	if (!editor) {
 	if (!editor) {
-		return
+		// Fallback for notebooks with no cells (no text editor active)
+		const activeNotebook = vscode.window.activeNotebookEditor
+		if (!activeNotebook) {
+			return
+		}
+		const filePath = activeNotebook.notebook.uri.fsPath
+		const diagnostics = convertVscodeDiagnostics(vscodeDiagnostics || [])
+		return { controller, commandContext: { selectedText: "", filePath, diagnostics, language: "" } }
 	}
 	}
 	// Use provided range if available, otherwise use current selection
 	// Use provided range if available, otherwise use current selection
 	// (vscode command passes an argument in the first param by default, so we need to ensure it's a Range object)
 	// (vscode command passes an argument in the first param by default, so we need to ensure it's a Range object)
@@ -50,6 +96,7 @@ export async function getContextForCommand(
 		diagnostics,
 		diagnostics,
 		language,
 		language,
 	}
 	}
+
 	return { controller, commandContext }
 	return { controller, commandContext }
 }
 }
 
 

+ 15 - 1
src/integrations/editor/DiffViewProvider.ts

@@ -276,6 +276,15 @@ export abstract class DiffViewProvider {
 		currentLine: number | undefined,
 		currentLine: number | undefined,
 	): Promise<void>
 	): Promise<void>
 
 
+	/**
+	 * Checks if the current file is a Jupyter notebook file.
+	 *
+	 * @returns true if the file has .ipynb extension
+	 */
+	protected isNotebookFile(): boolean {
+		return this.relPath?.toLowerCase().endsWith(".ipynb") ?? false
+	}
+
 	async saveChanges(): Promise<{
 	async saveChanges(): Promise<{
 		newProblemsMessage: string | undefined
 		newProblemsMessage: string | undefined
 		userEdits: string | undefined
 		userEdits: string | undefined
@@ -298,7 +307,12 @@ export abstract class DiffViewProvider {
 		// get text after save in case there is any auto-formatting done by the editor
 		// get text after save in case there is any auto-formatting done by the editor
 		const postSaveContent = (await this.getDocumentText()) || ""
 		const postSaveContent = (await this.getDocumentText()) || ""
 
 
-		await this.showFile(this.absolutePath)
+		// we need to open notebook files with Notebook editor if available.
+		// Currently, HostProvider opens it with Text editor. Not opening
+		// notebook files until we fix that.
+		if (!this.isNotebookFile()) {
+			await this.showFile(this.absolutePath)
+		}
 		await this.closeAllDiffViews()
 		await this.closeAllDiffViews()
 
 
 		const newProblems = await this.getNewDiagnosticProblems()
 		const newProblems = await this.getNewDiagnosticProblems()

+ 3 - 9
src/integrations/misc/extract-text.ts

@@ -7,6 +7,7 @@ import mammoth from "mammoth"
 import * as path from "path"
 import * as path from "path"
 // @ts-ignore-next-line
 // @ts-ignore-next-line
 import pdf from "pdf-parse/lib/pdf-parse"
 import pdf from "pdf-parse/lib/pdf-parse"
+import { sanitizeNotebookForLLM } from "./notebook-utils"
 
 
 export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string): Promise<string> {
 export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string): Promise<string> {
 	const detected = chardet.detect(fileBuffer)
 	const detected = chardet.detect(fileBuffer)
@@ -76,16 +77,9 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
 	const fileBuffer = await fs.readFile(filePath)
 	const fileBuffer = await fs.readFile(filePath)
 	const encoding = await detectEncoding(fileBuffer)
 	const encoding = await detectEncoding(fileBuffer)
 	const data = iconv.decode(fileBuffer, encoding)
 	const data = iconv.decode(fileBuffer, encoding)
-	const notebook = JSON.parse(data)
-	let extractedText = ""
 
 
-	for (const cell of notebook.cells) {
-		if ((cell.cell_type === "markdown" || cell.cell_type === "code") && cell.source) {
-			extractedText += cell.source.join("\n") + "\n"
-		}
-	}
-
-	return extractedText
+	// Return sanitized JSON for proper editing (enhanced notebook behavior is now always enabled)
+	return sanitizeNotebookForLLM(data)
 }
 }
 
 
 /**
 /**

+ 78 - 0
src/integrations/misc/notebook-utils.ts

@@ -0,0 +1,78 @@
+/**
+ * Shared utilities for processing Jupyter notebooks for LLM context.
+ * Used by both the context menu commands (addToCline, etc.) and file reading (extract-text.ts).
+ */
+
+/**
+ * Image MIME types that should be truncated in notebook outputs
+ */
+const IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"]
+
+/**
+ * Sanitizes the outputs of a single notebook cell by truncating image data.
+ * Keeps text outputs intact for context, only replaces binary image data with placeholders.
+ *
+ * @param cell A notebook cell object
+ * @returns The cell with sanitized outputs
+ */
+export function sanitizeCellOutputs(cell: Record<string, unknown>): Record<string, unknown> {
+	if (cell.cell_type !== "code" || !cell.outputs || !Array.isArray(cell.outputs)) {
+		return cell
+	}
+
+	const sanitizedOutputs = cell.outputs.map((output: Record<string, unknown>) => {
+		// Handle display_data and execute_result outputs with data field
+		if (output.data && typeof output.data === "object") {
+			const data = output.data as Record<string, unknown>
+			const sanitizedData = { ...data }
+
+			for (const mimeType of IMAGE_MIME_TYPES) {
+				if (mimeType in sanitizedData) {
+					sanitizedData[mimeType] = "[IMAGE DATA TRUNCATED]"
+				}
+			}
+
+			return { ...output, data: sanitizedData }
+		}
+		return output
+	})
+
+	return { ...cell, outputs: sanitizedOutputs }
+}
+
+/**
+ * Sanitizes a Jupyter notebook JSON by truncating verbose image data in cell outputs.
+ * This prevents flooding the LLM context with large base64-encoded images that are
+ * not useful for editing (outputs are regenerated when code runs).
+ *
+ * @param jsonString The raw notebook JSON string
+ * @returns Sanitized JSON string with image data truncated
+ */
+export function sanitizeNotebookForLLM(jsonString: string): string {
+	try {
+		const notebook = JSON.parse(jsonString)
+
+		if (!notebook.cells || !Array.isArray(notebook.cells)) {
+			return jsonString
+		}
+
+		notebook.cells = notebook.cells.map((cell: Record<string, unknown>) => sanitizeCellOutputs(cell))
+
+		return JSON.stringify(notebook, null, 1)
+	} catch {
+		// If parsing fails, return original string
+		return jsonString
+	}
+}
+
+/**
+ * Sanitizes a single notebook cell object and returns it as a JSON string.
+ * Used by context menu commands that work with individual cells.
+ *
+ * @param cell A notebook cell object
+ * @returns JSON string of the sanitized cell
+ */
+export function sanitizeCellForLLM(cell: Record<string, unknown>): string {
+	const sanitized = sanitizeCellOutputs(cell)
+	return JSON.stringify(sanitized, null, 2)
+}

+ 4 - 0
src/registry.ts

@@ -25,6 +25,10 @@ const ClineCommands = {
 	GenerateCommit: prefix + ".generateGitCommitMessage",
 	GenerateCommit: prefix + ".generateGitCommitMessage",
 	AbortCommit: prefix + ".abortGitCommitMessage",
 	AbortCommit: prefix + ".abortGitCommitMessage",
 	ReconstructTaskHistory: prefix + ".reconstructTaskHistory",
 	ReconstructTaskHistory: prefix + ".reconstructTaskHistory",
+	// Jupyter Notebook commands
+	JupyterGenerateCell: prefix + ".jupyterGenerateCell",
+	JupyterExplainCell: prefix + ".jupyterExplainCell",
+	JupyterImproveCell: prefix + ".jupyterImproveCell",
 }
 }
 
 
 /**
 /**