mirror of
https://github.com/cline/cline.git
synced 2026-09-15 13:02:17 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec141af32 | ||
|
|
a1dc73f932 | ||
|
|
ec68e7c2a8 | ||
|
|
52863261f4 | ||
|
|
6295cfb528 | ||
|
|
cc66f32a3a | ||
|
|
022ccfc419 | ||
|
|
e518663fa6 | ||
|
|
02daa25b6f | ||
|
|
f21b618531 | ||
|
|
71aac3ea7c | ||
|
|
e37e1a5714 | ||
|
|
d6f96e565c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
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.
|
||||
+45
-1
@@ -201,6 +201,24 @@
|
||||
"title": "Improve with 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",
|
||||
"title": "Open Walkthrough",
|
||||
@@ -304,6 +322,25 @@
|
||||
"when": "config.git.enabled && scmProvider == git && cline.isGeneratingCommit"
|
||||
}
|
||||
],
|
||||
"notebook/toolbar": [
|
||||
{
|
||||
"command": "cline.jupyterGenerateCell",
|
||||
"group": "navigation/add@1",
|
||||
"when": "notebookType == 'jupyter-notebook' && config.cline.enhancedNotebookInteractionEnabled"
|
||||
}
|
||||
],
|
||||
"notebook/cell/title": [
|
||||
{
|
||||
"command": "cline.jupyterExplainCell",
|
||||
"group": "inline@1",
|
||||
"when": "notebookType == 'jupyter-notebook' && config.cline.enhancedNotebookInteractionEnabled"
|
||||
},
|
||||
{
|
||||
"command": "cline.jupyterImproveCell",
|
||||
"group": "inline@2",
|
||||
"when": "notebookType == 'jupyter-notebook' && config.cline.enhancedNotebookInteractionEnabled"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "cline.generateGitCommitMessage",
|
||||
@@ -335,7 +372,14 @@
|
||||
},
|
||||
"configuration": {
|
||||
"title": "Cline",
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"cline.enhancedNotebookInteractionEnabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable enhanced Jupyter Notebook (.ipynb) support with cell-level context awareness. When enabled, Cline can read and edit notebook cells while preserving the JSON structure.",
|
||||
"order": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { singleFileDiagnosticsToProblemsString } from "@/integrations/diagnostics"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { Controller } from "../index"
|
||||
@@ -7,8 +8,9 @@ import { sendAddToInputEvent } from "../ui/subscribeToAddToInput"
|
||||
|
||||
// 'Add to Cline' context menu in editor and code action
|
||||
// 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 {}
|
||||
}
|
||||
|
||||
@@ -16,14 +18,28 @@ export async function addToCline(controller: Controller, request: CommandContext
|
||||
const fileMention = await getFileMentionFromPath(filePath)
|
||||
|
||||
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) {
|
||||
const problemsString = await singleFileDiagnosticsToProblemsString(filePath, request.diagnostics)
|
||||
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)
|
||||
|
||||
return {}
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
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({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to explain.",
|
||||
})
|
||||
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\`\`\``
|
||||
|
||||
// Add notebook context if provided (includes cell JSON)
|
||||
if (notebookContext) {
|
||||
Logger.log("Adding notebook context to explainWithCline task")
|
||||
prompt += notebookContext
|
||||
}
|
||||
|
||||
await controller.initTask(prompt)
|
||||
telemetryService.captureButtonClick("codeAction_explainCode", controller.task?.ulid)
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ export async function fixWithCline(controller: Controller, request: CommandConte
|
||||
const fileMention = await getFileMentionFromPath(filePath)
|
||||
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)
|
||||
|
||||
telemetryService.captureButtonClick("codeAction_fixWithCline", controller.task?.ulid)
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import { getFileMentionFromPath } from "@/core/mentions"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { CommandContext, Empty } from "@/shared/proto/index.cline"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
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({
|
||||
type: ShowMessageType.INFORMATION,
|
||||
message: "Please select some code to improve.",
|
||||
})
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { buildApiHandler } from "@core/api"
|
||||
|
||||
import { Empty } from "@shared/proto/cline/common"
|
||||
import {
|
||||
PlanActMode,
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ClineToolSpec } from "../spec"
|
||||
import { TASK_PROGRESS_PARAMETER } from "../types"
|
||||
import { SystemPromptContext, TASK_PROGRESS_PARAMETER } from "../types"
|
||||
|
||||
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
|
||||
[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.
|
||||
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`,
|
||||
* 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",
|
||||
},
|
||||
TASK_PROGRESS_PARAMETER,
|
||||
@@ -66,30 +98,7 @@ const NATIVE_NEXT_GEN: ClineToolSpec = {
|
||||
{
|
||||
name: "diff",
|
||||
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,
|
||||
],
|
||||
|
||||
@@ -95,6 +95,10 @@ export interface SystemPromptContext {
|
||||
readonly providerInfo: ApiProviderInfo
|
||||
readonly cwd?: string
|
||||
readonly ide: string
|
||||
readonly editorTabs?: {
|
||||
readonly open?: readonly string[]
|
||||
readonly visible?: readonly string[]
|
||||
}
|
||||
readonly supportsBrowserUse?: boolean
|
||||
readonly mcpHub?: McpHub
|
||||
readonly skills?: SkillMetadata[]
|
||||
|
||||
@@ -1778,10 +1778,21 @@ export class Task {
|
||||
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 = {
|
||||
cwd: this.cwd,
|
||||
ide,
|
||||
providerInfo,
|
||||
editorTabs,
|
||||
supportsBrowserUse,
|
||||
mcpHub: this.mcpHub,
|
||||
skills: availableSkills,
|
||||
|
||||
+148
-1
@@ -31,7 +31,7 @@ import { sendShowWebviewEvent } from "./core/controller/ui/subscribeToShowWebvie
|
||||
import { HookDiscoveryCache } from "./core/hooks/HookDiscoveryCache"
|
||||
import { HookProcessRegistry } from "./core/hooks/HookProcessRegistry"
|
||||
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 {
|
||||
disposeVscodeCommentReviewController,
|
||||
@@ -383,6 +383,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
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(commands.Walkthrough, async () => {
|
||||
@@ -433,6 +534,52 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
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) {
|
||||
console.log("Setting up vscode host providers...")
|
||||
|
||||
|
||||
@@ -90,4 +90,9 @@ export class ExternalDiffViewProvider extends DiffViewProvider {
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
this.activeDiffEditorId = undefined
|
||||
}
|
||||
|
||||
protected async switchToSpecializedEditor(): Promise<void> {
|
||||
// For external diff view provider, we don't have specialized editor support yet
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { DecorationController } from "@/hosts/vscode/DecorationController"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { arePathsEqual } from "@/utils/path"
|
||||
|
||||
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
|
||||
@@ -12,6 +13,10 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
private fadedOverlayController?: DecorationController
|
||||
private activeLineController?: DecorationController
|
||||
|
||||
// Temporary file management for notebook diff views
|
||||
private tempModifiedUri?: vscode.Uri
|
||||
private tempFileWatcher?: vscode.FileSystemWatcher
|
||||
|
||||
override async openDiffEditor(): Promise<void> {
|
||||
if (!this.absolutePath) {
|
||||
throw new Error("No file path set")
|
||||
@@ -96,6 +101,7 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
if (!this.activeDiffEditor || !this.activeDiffEditor.document) {
|
||||
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
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
this.activeDiffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
@@ -170,11 +176,156 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
return true
|
||||
}
|
||||
|
||||
protected override async switchToSpecializedEditor(): Promise<void> {
|
||||
if (!this.isNotebookFile() || !this.activeDiffEditor || !this.absolutePath) {
|
||||
Logger.log(
|
||||
`switchToSpecializedEditor: Early return - isNotebook: ${this.isNotebookFile()}, hasActiveDiffEditor: ${!!this.activeDiffEditor}, hasAbsolutePath: ${!!this.absolutePath}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const uri = vscode.Uri.file(this.absolutePath)
|
||||
const fileName = path.basename(uri.fsPath)
|
||||
|
||||
Logger.log(`Attempting to create notebook diff view for file: ${fileName}`)
|
||||
|
||||
// Check if Jupyter extension is available
|
||||
const jupyterExtension = vscode.extensions.getExtension("ms-toolsai.jupyter")
|
||||
if (!jupyterExtension) {
|
||||
Logger.log("Jupyter extension not found, cannot create notebook diff view")
|
||||
return
|
||||
}
|
||||
|
||||
if (!jupyterExtension.isActive) {
|
||||
Logger.log("Jupyter extension not active, activating...")
|
||||
await jupyterExtension.activate()
|
||||
}
|
||||
|
||||
// Create a proper notebook diff view by creating temporary files
|
||||
await this.createNotebookDiffView(uri, fileName)
|
||||
} catch (error) {
|
||||
Logger.error("Failed to create notebook diff view, continuing with text editor:", error)
|
||||
// Text editor remains active - no changes needed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a proper notebook diff view using temporary files
|
||||
*/
|
||||
private async createNotebookDiffView(uri: vscode.Uri, fileName: string): Promise<void> {
|
||||
try {
|
||||
Logger.log("Creating notebook diff view with temporary file...")
|
||||
|
||||
// Create temporary directory and file for modified content (right side)
|
||||
const tempDir = require("os").tmpdir()
|
||||
const timestamp = Date.now()
|
||||
const tempModifiedPath = path.join(tempDir, `cline-modified-${timestamp}-${fileName}`)
|
||||
|
||||
// Write current editor content to temporary file
|
||||
const currentContent = this.activeDiffEditor?.document.getText() ?? ""
|
||||
|
||||
try {
|
||||
// Attempt to parse the content as JSON
|
||||
JSON.parse(currentContent)
|
||||
} catch (error) {
|
||||
Logger.error(`Invalid JSON content for notebook file ${fileName}, skipping notebook diff view creation`)
|
||||
Logger.error(`JSON parse error: ${error}`)
|
||||
return
|
||||
}
|
||||
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempModifiedPath), new TextEncoder().encode(currentContent))
|
||||
|
||||
// Store temporary file URI for cleanup
|
||||
this.tempModifiedUri = vscode.Uri.file(tempModifiedPath)
|
||||
Logger.log(`Created temporary modified file: ${tempModifiedPath}`)
|
||||
|
||||
// Close current text diff editor
|
||||
// await this.closeCurrentTextDiffEditor()
|
||||
|
||||
// Set up file system watcher for synchronization
|
||||
this.setupTempDocumentListener()
|
||||
|
||||
// Open notebook diff view with original file (left) vs temporary file (right)
|
||||
Logger.log("Opening notebook diff view...")
|
||||
await vscode.commands.executeCommand(
|
||||
"vscode.diff",
|
||||
uri, // Left: original file
|
||||
this.tempModifiedUri, // Right: temporary file with modifications
|
||||
`${fileName}: Original ↔ Cline's Changes (Notebook)`,
|
||||
)
|
||||
|
||||
// Give VS Code a moment to open the notebook diff view
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
Logger.log("Notebook diff view opened successfully")
|
||||
} catch (error) {
|
||||
Logger.error(`Error creating notebook diff view: ${error}`)
|
||||
// Clean up on error
|
||||
await this.cleanupTempFiles()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up file system watcher for temporary file synchronization
|
||||
*/
|
||||
private setupTempDocumentListener(): void {
|
||||
if (this.tempModifiedUri) {
|
||||
this.tempFileWatcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(this.tempModifiedUri.fsPath, "*"),
|
||||
)
|
||||
|
||||
this.tempFileWatcher.onDidChange(async () => {
|
||||
await this.syncTempFileToActiveDiffEditor()
|
||||
})
|
||||
|
||||
Logger.log(`File system watcher set up for temp file: ${this.tempModifiedUri.fsPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync changes from temporary file back to active diff editor only
|
||||
*/
|
||||
private async syncTempFileToActiveDiffEditor(): Promise<void> {
|
||||
if (this.tempModifiedUri && this.activeDiffEditor && this.activeDiffEditor.document) {
|
||||
try {
|
||||
const tempContent = await vscode.workspace.fs.readFile(this.tempModifiedUri)
|
||||
const tempContentString = new TextDecoder().decode(tempContent)
|
||||
|
||||
// Update text editor content to match temporary file
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
const fullRange = new vscode.Range(0, 0, this.activeDiffEditor.document.lineCount, 0)
|
||||
edit.replace(this.activeDiffEditor.document.uri, fullRange, tempContentString)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
Logger.log("Synced temp file changes back to active diff editor")
|
||||
} catch (error) {
|
||||
Logger.error("Failed to sync temp file to active diff editor:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async closeAllDiffViews(): Promise<void> {
|
||||
// Close all the cline diff views.
|
||||
// Close all the cline diff views (both text and notebook diff views).
|
||||
const tabs = vscode.window.tabGroups.all
|
||||
.flatMap((tg) => tg.tabs)
|
||||
.filter((tab) => tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME)
|
||||
.filter((tab) => {
|
||||
// Regular Cline text diff views
|
||||
if (tab.input instanceof vscode.TabInputTextDiff && tab.input?.original?.scheme === DIFF_VIEW_URI_SCHEME) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Notebook diff views created by createNotebookDiffView()
|
||||
if (
|
||||
tab.input instanceof vscode.TabInputNotebookDiff &&
|
||||
tab.input?.modified?.fsPath?.includes("cline-modified-")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
for (const tab of tabs) {
|
||||
// trying to close dirty views results in save popup
|
||||
if (!tab.isDirty) {
|
||||
@@ -188,8 +339,34 @@ export class VscodeDiffViewProvider extends DiffViewProvider {
|
||||
}
|
||||
|
||||
protected override async resetDiffView(): Promise<void> {
|
||||
// Clean up temporary files and listeners (basic cleanup for now)
|
||||
await this.cleanupTempFiles()
|
||||
|
||||
this.activeDiffEditor = undefined
|
||||
this.fadedOverlayController = undefined
|
||||
this.activeLineController = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary files and watchers (basic implementation)
|
||||
*/
|
||||
private async cleanupTempFiles(): Promise<void> {
|
||||
// Dispose file watcher first
|
||||
if (this.tempFileWatcher) {
|
||||
this.tempFileWatcher.dispose()
|
||||
this.tempFileWatcher = undefined
|
||||
}
|
||||
|
||||
// Clean up temporary file
|
||||
if (this.tempModifiedUri) {
|
||||
try {
|
||||
await vscode.workspace.fs.delete(this.tempModifiedUri)
|
||||
Logger.log(`Cleaned up temporary file: ${this.tempModifiedUri.fsPath}`)
|
||||
} catch (error) {
|
||||
// Log but don't throw - cleanup should be non-blocking
|
||||
Logger.log(`Failed to cleanup temporary file: ${error}`)
|
||||
}
|
||||
this.tempModifiedUri = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,49 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import { sanitizeCellForLLM } from "@/integrations/misc/notebook-utils"
|
||||
import { ExtensionRegistryInfo } from "@/registry"
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { CommandContext } from "@/shared/proto/index.cline"
|
||||
import { Controller } from "../../core/controller"
|
||||
import { WebviewProvider } from "../../core/webview"
|
||||
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
|
||||
* @param range Optional range to use instead of current selection
|
||||
@@ -34,7 +73,14 @@ export async function getContextForCommand(
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
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
|
||||
// (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,
|
||||
language,
|
||||
}
|
||||
|
||||
return { controller, commandContext }
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,15 @@ export abstract class DiffViewProvider {
|
||||
*/
|
||||
protected abstract resetDiffView(): Promise<void>
|
||||
|
||||
/**
|
||||
* Switches to a specialized editor for specific file types after final content is available.
|
||||
* Called automatically by the `update` method when `isFinal` is true.
|
||||
*
|
||||
* For example, switches to Jupyter notebook editor for .ipynb files to provide
|
||||
* enhanced editing experience with proper notebook cell rendering.
|
||||
*/
|
||||
protected abstract switchToSpecializedEditor(): Promise<void>
|
||||
|
||||
async update(
|
||||
accumulatedContent: string,
|
||||
isFinal: boolean,
|
||||
@@ -221,6 +230,9 @@ export abstract class DiffViewProvider {
|
||||
accumulatedContent += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to specialized editor for specific file types (e.g., Jupyter for .ipynb)
|
||||
await this.switchToSpecializedEditor()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +258,15 @@ export abstract class DiffViewProvider {
|
||||
currentLine: number | undefined,
|
||||
): 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<{
|
||||
newProblemsMessage: string | undefined
|
||||
userEdits: string | undefined
|
||||
@@ -268,7 +289,12 @@ export abstract class DiffViewProvider {
|
||||
// get text after save in case there is any auto-formatting done by the editor
|
||||
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()
|
||||
|
||||
const newProblems = await this.getNewDiagnosticProblems()
|
||||
|
||||
@@ -113,4 +113,9 @@ export class FileEditProvider extends DiffViewProvider {
|
||||
// Clean up the in-memory document content
|
||||
this.documentContent = undefined
|
||||
}
|
||||
|
||||
protected async switchToSpecializedEditor(): Promise<void> {
|
||||
// No-op: File-system-only provider doesn't support visual specialized editors
|
||||
// All operations are performed directly on the file system
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import mammoth from "mammoth"
|
||||
import * as path from "path"
|
||||
// @ts-ignore-next-line
|
||||
import pdf from "pdf-parse/lib/pdf-parse"
|
||||
import { sanitizeNotebookForLLM } from "./notebook-utils"
|
||||
|
||||
export async function detectEncoding(fileBuffer: Buffer, fileExtension?: string): Promise<string> {
|
||||
const detected = chardet.detect(fileBuffer)
|
||||
@@ -76,16 +77,9 @@ async function extractTextFromIPYNB(filePath: string): Promise<string> {
|
||||
const fileBuffer = await fs.readFile(filePath)
|
||||
const encoding = await detectEncoding(fileBuffer)
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -24,6 +24,10 @@ const ClineCommands = {
|
||||
GenerateCommit: prefix + ".generateGitCommitMessage",
|
||||
AbortCommit: prefix + ".abortGitCommitMessage",
|
||||
ReconstructTaskHistory: prefix + ".reconstructTaskHistory",
|
||||
// Jupyter Notebook commands
|
||||
JupyterGenerateCell: prefix + ".jupyterGenerateCell",
|
||||
JupyterExplainCell: prefix + ".jupyterExplainCell",
|
||||
JupyterImproveCell: prefix + ".jupyterImproveCell",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,7 +169,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<VSCodeCheckbox
|
||||
checked={enableCheckpointsSetting}
|
||||
onChange={(e: any) => {
|
||||
|
||||
Reference in New Issue
Block a user