Compare commits

...

1 Commits

Author SHA1 Message Date
Igor Tceglevskii 154aab81e9 feat: add "Add Last Command to Cline" terminal menu 2025-11-25 20:25:14 -08:00
5 changed files with 288 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Add "Add Last Command to Cline" terminal menu
+11 -1
View File
@@ -174,6 +174,11 @@
"title": "Add to Cline",
"category": "Cline"
},
{
"command": "cline.addTerminalCommandToChat",
"title": "Add Last Command to Cline",
"category": "Cline"
},
{
"command": "cline.focusChatInput",
"title": "Jump to Chat Input",
@@ -272,7 +277,12 @@
"terminal/context": [
{
"command": "cline.addTerminalOutputToChat",
"group": "navigation"
"group": "navigation@1"
},
{
"command": "cline.addTerminalCommandToChat",
"group": "navigation@2",
"when": "cline.terminalHasCommandHistory"
}
],
"scm/title": [
+94
View File
@@ -35,6 +35,7 @@ import { focusChatInput, getContextForCommand } from "./hosts/vscode/commandUtil
import { abortCommitGeneration, generateCommitMsg } from "./hosts/vscode/commit-message-generator"
import { VscodeDiffViewProvider } from "./hosts/vscode/VscodeDiffViewProvider"
import { VscodeWebviewProvider } from "./hosts/vscode/VscodeWebviewProvider"
import { CommandHistoryTracker } from "./integrations/terminal/CommandHistoryTracker"
import { ExtensionRegistryInfo } from "./registry"
import { AuthService } from "./services/auth/AuthService"
import { LogoutReason } from "./services/auth/types"
@@ -84,6 +85,29 @@ export async function activate(context: vscode.ExtensionContext) {
Logger.log("Cline extension activated")
// Initialize command history tracker for "Add Command to Cline" feature
const commandHistoryTracker = new CommandHistoryTracker(() => {
// Update context key when command history changes
const terminal = vscode.window.activeTerminal
const hasHistory = terminal && commandHistoryTracker.hasHistory(terminal)
vscode.commands.executeCommand("setContext", "cline.terminalHasCommandHistory", hasHistory)
})
context.subscriptions.push(commandHistoryTracker)
// Update context when active terminal changes
context.subscriptions.push(
vscode.window.onDidChangeActiveTerminal(() => {
const terminal = vscode.window.activeTerminal
const hasHistory = terminal && commandHistoryTracker.hasHistory(terminal)
vscode.commands.executeCommand("setContext", "cline.terminalHasCommandHistory", hasHistory)
}),
)
// Initialize context on activation
const initialTerminal = vscode.window.activeTerminal
const initialHasHistory = initialTerminal && commandHistoryTracker.hasHistory(initialTerminal)
vscode.commands.executeCommand("setContext", "cline.terminalHasCommandHistory", initialHasHistory)
const testModeWatchers = await initializeTestMode(webview)
// Initialize test mode and add disposables to context
context.subscriptions.push(...testModeWatchers)
@@ -207,6 +231,9 @@ export async function activate(context: vscode.ExtensionContext) {
await sendAddToInputEvent(`Terminal output:\n\`\`\`\n${terminalContents}\n\`\`\``)
// Telemetry
telemetryService.captureButtonClick("command_addTerminalOutputToChat")
console.log("addSelectedTerminalOutputToChat", terminalContents, terminal.name)
} catch (error) {
// Ensure clipboard is restored even if an error occurs
@@ -220,6 +247,73 @@ export async function activate(context: vscode.ExtensionContext) {
}),
)
// Register command for terminal command decoration context menu (shell integration)
context.subscriptions.push(
vscode.commands.registerCommand(commands.AddTerminalCommandToChat, async (terminalArg?: any) => {
try {
// VSCode passes the Terminal object as the argument
const terminal = terminalArg || vscode.window.activeTerminal
if (!terminal) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "No active terminal found",
})
return
}
// Get the most recent command from history
// Note: This should always exist since menu only appears when cline.terminalHasCommandHistory is true
const commandInfo = commandHistoryTracker.getLatestCommand(terminal)
if (!commandInfo) {
// This shouldn't happen given the menu visibility condition, but handle gracefully
console.error("Menu shown but no command history available")
return
}
// Build formatted message
let message = `Terminal Command Execution:\n`
message += `Command: \`${commandInfo.commandLine}\`\n`
message += `Directory: \`${commandInfo.cwd || "Unknown"}\`\n`
message += `Time: ${commandInfo.timestamp.toLocaleString()}\n`
message += `Duration: ${commandInfo.duration}ms\n`
if (commandInfo.exitCode !== undefined) {
const status = commandInfo.exitCode === 0 ? "Success" : `Failed`
message += `Exit Code: ${commandInfo.exitCode} (${status})\n`
} else {
message += `Status: Still running or unknown\n`
}
if (commandInfo.cleanOutput.trim()) {
message += `\nOutput:\n\`\`\`\n${commandInfo.cleanOutput.trim()}\n\`\`\``
}
// Show chat and send message
await focusChatInput()
await sendAddToInputEvent(message)
// Telemetry
telemetryService.captureButtonClick("command_addTerminalCommandToChat")
console.log("addTerminalCommandToChat result:", {
commandLine: commandInfo.commandLine,
cwd: commandInfo.cwd,
exitCode: commandInfo.exitCode,
outputLength: commandInfo.cleanOutput.length,
duration: commandInfo.duration,
})
} catch (error) {
console.error("Error adding terminal command to chat:", error)
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message: "Failed to add terminal command to chat",
})
}
}),
)
// Register code action provider
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
@@ -0,0 +1,177 @@
import * as vscode from "vscode"
import { stripAnsi } from "./ansiUtils"
interface CommandHistoryItem {
commandLine: string
cwd?: string
exitCode?: number
output: string
startTime: number
endTime?: number
}
interface CommandInfo {
commandLine: string
cwd?: string
exitCode?: number
cleanOutput: string
duration: number
timestamp: Date
}
/**
* Tracks terminal command execution history using VSCode's shell integration API.
* Listens for command start/end events and maintains a history of recent commands per terminal.
*/
export class CommandHistoryTracker {
private commandHistory = new Map<vscode.Terminal, CommandHistoryItem[]>()
private readonly maxHistoryPerTerminal = 20
private disposables: vscode.Disposable[] = []
private onHistoryChangedCallback?: () => void
constructor(onHistoryChanged?: () => void) {
this.onHistoryChangedCallback = onHistoryChanged
this.setupEventListeners()
}
private setupEventListeners(): void {
// Track command start
try {
const onStartExecution = (vscode.window as any).onDidStartTerminalShellExecution
if (onStartExecution) {
this.disposables.push(
onStartExecution((event: any) => {
this.handleCommandStart(event)
}),
)
}
} catch (error) {
console.log("Shell execution tracking not available:", error)
}
// Track command end
try {
const onEndExecution = (vscode.window as any).onDidEndTerminalShellExecution
if (onEndExecution) {
this.disposables.push(
onEndExecution((event: any) => {
this.handleCommandEnd(event)
}),
)
}
} catch (error) {
console.log("Shell execution end tracking not available:", error)
}
// Clean up when terminal closes
this.disposables.push(
vscode.window.onDidCloseTerminal((terminal) => {
this.commandHistory.delete(terminal)
}),
)
}
private handleCommandStart(event: any): void {
if (!this.commandHistory.has(event.terminal)) {
this.commandHistory.set(event.terminal, [])
}
const history = this.commandHistory.get(event.terminal)!
const item: CommandHistoryItem = {
commandLine: event.execution.commandLine.value,
cwd: event.execution.cwd?.fsPath,
exitCode: undefined,
output: "",
startTime: Date.now(),
endTime: undefined,
}
history.push(item)
// Keep only last N commands per terminal
if (history.length > this.maxHistoryPerTerminal) {
history.shift()
}
// Notify that history changed
this.onHistoryChangedCallback?.()
// Collect output asynchronously
;(async () => {
try {
for await (const data of event.execution.read()) {
item.output += data
}
} catch (e) {
console.error("Error reading command output:", e)
}
})()
}
private handleCommandEnd(event: any): void {
const history = this.commandHistory.get(event.terminal)
if (history && history.length > 0) {
const lastItem = history[history.length - 1]
if (lastItem.commandLine === event.execution.commandLine.value) {
lastItem.exitCode = event.exitCode
lastItem.endTime = Date.now()
}
}
}
/**
* Get the most recent command from a terminal's history
*/
public getLatestCommand(terminal: vscode.Terminal): CommandInfo | undefined {
const history = this.commandHistory.get(terminal)
if (!history || history.length === 0) {
return undefined
}
const lastCommand = history[history.length - 1]
return {
commandLine: lastCommand.commandLine,
cwd: lastCommand.cwd,
exitCode: lastCommand.exitCode,
cleanOutput: stripAnsi(lastCommand.output),
duration: lastCommand.endTime ? lastCommand.endTime - lastCommand.startTime : Date.now() - lastCommand.startTime,
timestamp: new Date(lastCommand.startTime),
}
}
/**
* Check if a terminal has any command history
*/
public hasHistory(terminal: vscode.Terminal): boolean {
const history = this.commandHistory.get(terminal)
return history !== undefined && history.length > 0
}
/**
* Get all commands for a terminal
*/
public getHistory(terminal: vscode.Terminal): CommandInfo[] {
const history = this.commandHistory.get(terminal)
if (!history) {
return []
}
return history.map((item) => ({
commandLine: item.commandLine,
cwd: item.cwd,
exitCode: item.exitCode,
cleanOutput: stripAnsi(item.output),
duration: item.endTime ? item.endTime - item.startTime : Date.now() - item.startTime,
timestamp: new Date(item.startTime),
}))
}
/**
* Clean up resources
*/
public dispose(): void {
this.disposables.forEach((d) => d.dispose())
this.disposables = []
this.commandHistory.clear()
}
}
+1
View File
@@ -15,6 +15,7 @@ const ClineCommands = {
HistoryButton: prefix + ".historyButtonClicked",
AccountButton: prefix + ".accountButtonClicked",
TerminalOutput: prefix + ".addTerminalOutputToChat",
AddTerminalCommandToChat: prefix + ".addTerminalCommandToChat",
AddToChat: prefix + ".addToChat",
FixWithCline: prefix + ".fixWithCline",
ExplainCode: prefix + ".explainCode",