Skip to content

Commit b4a2d5a

Browse files
committed
Add 'Add to Cline' and 'Fix with Cline' menu options
1 parent 7f2de39 commit b4a2d5a

File tree

6 files changed

+261
-1
lines changed

6 files changed

+261
-1
lines changed

package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@
104104
"command": "cline.openDocumentation",
105105
"title": "Documentation",
106106
"icon": "$(book)"
107+
},
108+
{
109+
"command": "cline.addToChat",
110+
"title": "Add to Cline",
111+
"category": "Cline"
112+
},
113+
{
114+
"command": "cline.addTerminalOutputToChat",
115+
"title": "Add to Cline",
116+
"category": "Cline"
117+
},
118+
{
119+
"command": "cline.fixWithCline",
120+
"title": "Fix with Cline",
121+
"category": "Cline"
107122
}
108123
],
109124
"menus": {
@@ -143,6 +158,19 @@
143158
"group": "navigation@7",
144159
"when": "view == claude-dev.SidebarProvider"
145160
}
161+
],
162+
"editor/context": [
163+
{
164+
"command": "cline.addToChat",
165+
"group": "navigation",
166+
"when": "editorHasSelection"
167+
}
168+
],
169+
"terminal/context": [
170+
{
171+
"command": "cline.addTerminalOutputToChat",
172+
"group": "navigation"
173+
}
146174
]
147175
},
148176
"configuration": {

src/core/mentions/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
7272
}
7373
}
7474

75-
for (const mention of mentions) {
75+
// Filter out duplicate mentions while preserving order
76+
const uniqueMentions = Array.from(new Set(mentions))
77+
78+
for (const mention of uniqueMentions) {
7679
if (mention.startsWith("http")) {
7780
let result: string
7881
if (launchBrowserError) {

src/core/webview/ClineProvider.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
4040
import { getTotalTasksSize } from "../../utils/storage"
4141
import { ConversationTelemetryService } from "../../services/telemetry/ConversationTelemetryService"
4242
import { GlobalFileNames } from "../../global-constants"
43+
import delay from "delay"
4344

4445
/*
4546
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -1748,6 +1749,93 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
17481749
return models
17491750
}
17501751

1752+
// Context menus and code actions
1753+
1754+
getFileMentionFromPath(filePath: string) {
1755+
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
1756+
if (!cwd) {
1757+
return "@/" + filePath
1758+
}
1759+
const relativePath = path.relative(cwd, filePath)
1760+
return "@/" + relativePath
1761+
}
1762+
1763+
// 'Add to Cline' context menu in editor and code action
1764+
async addSelectedCodeToChat(code: string, filePath: string, languageId: string) {
1765+
// Ensure the sidebar view is visible
1766+
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
1767+
await delay(100)
1768+
1769+
// Post message to webview with the selected code
1770+
const fileMention = this.getFileMentionFromPath(filePath)
1771+
await this.postMessageToWebview({
1772+
type: "addToInput",
1773+
text: `${fileMention}\n\`\`\`\n${code}\n\`\`\``,
1774+
})
1775+
1776+
console.log("addSelectedCodeToChat", code, filePath, languageId)
1777+
}
1778+
1779+
// 'Add to Cline' context menu in Terminal
1780+
async addSelectedTerminalOutputToChat(output: string, terminalName: string) {
1781+
// Ensure the sidebar view is visible
1782+
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
1783+
await delay(100)
1784+
1785+
// Post message to webview with the selected terminal output
1786+
// await this.postMessageToWebview({
1787+
// type: "addSelectedTerminalOutput",
1788+
// output,
1789+
// terminalName
1790+
// })
1791+
1792+
await this.postMessageToWebview({
1793+
type: "addToInput",
1794+
text: `Terminal output:\n\`\`\`\n${output}\n\`\`\``,
1795+
})
1796+
1797+
console.log("addSelectedTerminalOutputToChat", output, terminalName)
1798+
}
1799+
1800+
// 'Fix with Cline' in code actions
1801+
async fixWithCline(code: string, filePath: string, languageId: string, diagnostics: vscode.Diagnostic[]) {
1802+
// Ensure the sidebar view is visible
1803+
await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
1804+
await delay(100)
1805+
1806+
const fileMention = this.getFileMentionFromPath(filePath)
1807+
1808+
let problemsString = ""
1809+
for (const diagnostic of diagnostics) {
1810+
let label: string
1811+
switch (diagnostic.severity) {
1812+
case vscode.DiagnosticSeverity.Error:
1813+
label = "Error"
1814+
break
1815+
case vscode.DiagnosticSeverity.Warning:
1816+
label = "Warning"
1817+
break
1818+
case vscode.DiagnosticSeverity.Information:
1819+
label = "Information"
1820+
break
1821+
case vscode.DiagnosticSeverity.Hint:
1822+
label = "Hint"
1823+
break
1824+
default:
1825+
label = "Diagnostic"
1826+
}
1827+
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
1828+
const source = diagnostic.source ? `${diagnostic.source} ` : ""
1829+
problemsString += `\n- [${source}${label}] Line ${line}: ${diagnostic.message}`
1830+
}
1831+
1832+
await this.initClineWithTask(
1833+
`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString.trim()}`,
1834+
)
1835+
1836+
console.log("fixWithCline", code, filePath, languageId, diagnostics, problemsString)
1837+
}
1838+
17511839
// Task history
17521840

17531841
async getTaskWithId(id: string): Promise<{

src/extension.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,134 @@ export function activate(context: vscode.ExtensionContext) {
193193
}
194194
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
195195

196+
context.subscriptions.push(
197+
vscode.commands.registerCommand("cline.addToChat", async (range?: vscode.Range) => {
198+
const editor = vscode.window.activeTextEditor
199+
if (!editor) {
200+
return
201+
}
202+
203+
// Use provided range if available, otherwise use current selection
204+
// (vscode command passes an argument in the first param by default, so we need to ensure it's a Range object)
205+
const textRange = range instanceof vscode.Range ? range : editor.selection
206+
const selectedText = editor.document.getText(textRange)
207+
208+
if (!selectedText) {
209+
return
210+
}
211+
212+
// Get the file path and language ID
213+
const filePath = editor.document.uri.fsPath
214+
const languageId = editor.document.languageId
215+
216+
// Send to sidebar provider
217+
await sidebarProvider.addSelectedCodeToChat(selectedText, filePath, languageId)
218+
}),
219+
)
220+
221+
context.subscriptions.push(
222+
vscode.commands.registerCommand("cline.addTerminalOutputToChat", async () => {
223+
const terminal = vscode.window.activeTerminal
224+
if (!terminal) {
225+
return
226+
}
227+
228+
// Save current clipboard content
229+
const tempCopyBuffer = await vscode.env.clipboard.readText()
230+
231+
try {
232+
// Copy the *existing* terminal selection (without selecting all)
233+
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
234+
235+
// Get copied content
236+
let terminalContents = (await vscode.env.clipboard.readText()).trim()
237+
238+
// Restore original clipboard content
239+
await vscode.env.clipboard.writeText(tempCopyBuffer)
240+
241+
if (!terminalContents) {
242+
// No terminal content was copied (either nothing selected or some error)
243+
return
244+
}
245+
246+
// [Optional] Any additional logic to process multi-line content can remain here
247+
// For example:
248+
/*
249+
const lines = terminalContents.split("\n")
250+
const lastLine = lines.pop()?.trim()
251+
if (lastLine) {
252+
let i = lines.length - 1
253+
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
254+
i--
255+
}
256+
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
257+
}
258+
*/
259+
260+
// Send to sidebar provider
261+
await sidebarProvider.addSelectedTerminalOutputToChat(terminalContents, terminal.name)
262+
} catch (error) {
263+
// Ensure clipboard is restored even if an error occurs
264+
await vscode.env.clipboard.writeText(tempCopyBuffer)
265+
console.error("Error getting terminal contents:", error)
266+
vscode.window.showErrorMessage("Failed to get terminal contents")
267+
}
268+
}),
269+
)
270+
271+
// Register code action provider
272+
context.subscriptions.push(
273+
vscode.languages.registerCodeActionsProvider(
274+
"*",
275+
new (class implements vscode.CodeActionProvider {
276+
public static readonly providedCodeActionKinds = [vscode.CodeActionKind.QuickFix]
277+
278+
provideCodeActions(
279+
document: vscode.TextDocument,
280+
range: vscode.Range,
281+
context: vscode.CodeActionContext,
282+
): vscode.CodeAction[] {
283+
// Always offer both actions
284+
const addAction = new vscode.CodeAction("Add to Cline", vscode.CodeActionKind.QuickFix)
285+
addAction.command = {
286+
command: "cline.addToChat",
287+
title: "Add to Cline",
288+
arguments: [range],
289+
}
290+
291+
const fixAction = new vscode.CodeAction("Fix with Cline", vscode.CodeActionKind.QuickFix)
292+
fixAction.command = {
293+
command: "cline.fixWithCline",
294+
title: "Fix with Cline",
295+
arguments: [range, context.diagnostics],
296+
}
297+
298+
return [addAction, fixAction]
299+
}
300+
})(),
301+
{
302+
providedCodeActionKinds: [vscode.CodeActionKind.QuickFix],
303+
},
304+
),
305+
)
306+
307+
// Register the command handler
308+
context.subscriptions.push(
309+
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: any[]) => {
310+
const editor = vscode.window.activeTextEditor
311+
if (!editor) {
312+
return
313+
}
314+
315+
const selectedText = editor.document.getText(range)
316+
const filePath = editor.document.uri.fsPath
317+
const languageId = editor.document.languageId
318+
319+
// Send to sidebar provider with diagnostics
320+
await sidebarProvider.fixWithCline(selectedText, filePath, languageId, diagnostics)
321+
}),
322+
)
323+
196324
return createClineAPI(outputChannel, sidebarProvider)
197325
}
198326

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface ExtensionMessage {
3939
| "userCreditsUsage"
4040
| "userCreditsPayments"
4141
| "totalTasksSize"
42+
| "addToInput"
4243
text?: string
4344
action?:
4445
| "chatButtonClicked"

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,18 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
453453
setSelectedImages((prevImages) => [...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE))
454454
}
455455
break
456+
case "addToInput":
457+
setInputValue((prevValue) => {
458+
const newText = message.text ?? ""
459+
return prevValue ? `${prevValue}\n${newText}` : newText
460+
})
461+
// Add scroll to bottom after state update
462+
setTimeout(() => {
463+
if (textAreaRef.current) {
464+
textAreaRef.current.scrollTop = textAreaRef.current.scrollHeight
465+
}
466+
}, 0)
467+
break
456468
case "invoke":
457469
switch (message.invoke!) {
458470
case "sendMessage":

0 commit comments

Comments
 (0)