Compare commits

...
Author SHA1 Message Date
Arafatkatze f0f54e156e Adding terminal option 2025-10-01 15:01:37 -07:00
Arafatkatze 7e2097ad70 Minor UX fixes for voice mode 2025-09-25 20:54:02 -07:00
Arafatkatze b5816f8b07 Adding shell type to the telemetry 2025-09-24 14:01:38 -07:00
Arafatkatze 6d70f11986 Adding shell type to the telemetry 2025-09-24 12:16:53 -07:00
Arafatkatze edfa923a76 Adding shell type to the telemetry 2025-09-24 12:12:37 -07:00
5 changed files with 129 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Adding exec based terminal
+36 -6
View File
@@ -49,7 +49,14 @@ import { ApiConfiguration } from "@shared/api"
import { findLast, findLastIndex } from "@shared/array"
import { combineApiRequests } from "@shared/combineApiRequests"
import { combineCommandSequences } from "@shared/combineCommandSequences"
import { ClineApiReqCancelReason, ClineApiReqInfo, ClineAsk, ClineMessage, ClineSay } from "@shared/ExtensionMessage"
import {
ClineApiReqCancelReason,
ClineApiReqInfo,
ClineAsk,
ClineMessage,
ClineSay,
COMMAND_CANCEL_TOKEN,
} from "@shared/ExtensionMessage"
import { HistoryItem } from "@shared/HistoryItem"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay } from "@shared/Languages"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
@@ -1035,6 +1042,7 @@ export class Task {
let userFeedback: { text?: string; images?: string[]; files?: string[] } | undefined
let didContinue = false
let didCancelViaUi = false
// Chunked terminal output buffering
const CHUNK_LINE_COUNT = 20
@@ -1072,12 +1080,33 @@ export class Task {
try {
const { response, text, images, files } = await this.ask("command_output", chunk)
if (response === "yesButtonClicked") {
// Track when user clicks "Process while Running"
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
// proceed while running - but still capture user feedback if provided
if (text || (images && images.length > 0) || (files && files.length > 0)) {
userFeedback = { text, images, files }
}
} else if (response === "noButtonClicked" && text === COMMAND_CANCEL_TOKEN) {
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.CANCELLED)
didCancelViaUi = true
didContinue = true
userFeedback = undefined
if (typeof (process as any).terminate === "function") {
try {
;(process as any).terminate()
} catch (error) {
Logger.warn("Failed to terminate background terminal process", error)
}
}
try {
terminalInfo.terminal.sendText("\u0003", false)
} catch (error) {
Logger.warn("Failed to send Ctrl+C to terminal during cancellation", error)
}
await this.say(
"command_output",
"Command cancelled. It will keep running in the terminal if you need to monitor it manually.",
)
outputBuffer = []
outputBufferSize = 0
} else {
userFeedback = { text, images, files }
}
@@ -1086,14 +1115,12 @@ export class Task {
} catch {
Logger.error("Error while asking for command output")
} finally {
// Clear the stuck timer
if (bufferStuckTimer) {
clearTimeout(bufferStuckTimer)
bufferStuckTimer = null
}
chunkEnroute = false
// If more output accumulated while chunkEnroute, flush again
if (outputBuffer.length > 0) {
if (!didCancelViaUi && outputBuffer.length > 0) {
await flushBuffer()
}
}
@@ -1108,6 +1135,9 @@ export class Task {
const outputLines: string[] = []
process.on("line", async (line) => {
if (didCancelViaUi) {
return
}
outputLines.push(line)
if (!didContinue) {
@@ -162,6 +162,10 @@ export class TerminalManager {
terminalInfo.busy = true
terminalInfo.lastCommand = command
const process = new TerminalProcess()
// Pass the shell path to the process for telemetry
process.setShellPath(terminalInfo.shellPath)
this.processes.set(terminalInfo.id, process)
process.once("completed", () => {
+75 -7
View File
@@ -24,8 +24,76 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
private lastRetrievedIndex: number = 0
isHot: boolean = false
private hotTimer: NodeJS.Timeout | null = null
private shellPath?: string
/**
* Set the shell path for this terminal process
*/
public setShellPath(shellPath?: string): void {
this.shellPath = shellPath
}
/**
* Detect shell from environment variables as a fallback
*/
private detectShellFromEnvironment(): string | undefined {
// Try to detect shell from environment variables
if (process.platform === "win32") {
// On Windows, check COMSPEC
return process.env.COMSPEC || undefined
} else {
// On Unix-like systems, check SHELL
return process.env.SHELL || undefined
}
}
/**
* Extract a normalized shell name from the shell path
*/
private extractShellName(shellPath?: string): string {
if (!shellPath) return "unknown"
// Extract just the filename from the path
const pathParts = shellPath.split(/[/\\]/)
const shellName = pathParts[pathParts.length - 1].toLowerCase()
// Normalize common variations
// Order matters: check more specific names before generic ones
if (shellName.includes("pwsh")) return "powershell-7"
if (shellName.includes("powershell")) return "powershell"
if (shellName.includes("bash")) return "bash"
if (shellName.includes("zsh")) return "zsh"
if (shellName.includes("fish")) return "fish"
if (shellName.includes("dash")) return "dash"
if (shellName.includes("tcsh")) return "tcsh"
if (shellName.includes("ksh")) return "ksh"
if (shellName.includes("csh")) return "csh" // Check 'csh' before 'sh'
if (shellName.includes("cmd")) return "cmd"
if (shellName.includes("sh")) return "sh" // Check 'sh' last as it's a substring of many shells
// Remove .exe extension on Windows
return shellName.replace(/\.exe$/, "")
}
async run(terminal: vscode.Terminal, command: string) {
// Get the actual shell that VSCode is using for this terminal
// First try shellPath (set from TerminalInfo), then try to get from terminal.creationOptions
let shellPathToUse = this.shellPath
// If shellPath not set, try to get the actual shell from the terminal's creation options
if (!shellPathToUse && terminal.creationOptions) {
const options = terminal.creationOptions as any
// VSCode stores the shell path in creationOptions
shellPathToUse = options.shellPath || options.shellArgs?.shell
}
// Final fallback to environment if we still don't have it
if (!shellPathToUse) {
shellPathToUse = this.detectShellFromEnvironment()
}
const shellName = this.extractShellName(shellPathToUse)
// When command does not produce any output, we can assume the shell integration API failed and as a fallback return the current terminal contents
const returnCurrentTerminalContents = async () => {
try {
@@ -192,18 +260,18 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// the command process is finished, let's check the output to see if we need to use the terminal capture fallback
if (!this.fullOutput.trim()) {
// No output captured via shell integration, trying fallback
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.TIMEOUT)
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.TIMEOUT, shellName)
await returnCurrentTerminalContents()
// Check if fallback worked
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
telemetryService.captureTerminalExecution(true, "clipboard")
telemetryService.captureTerminalExecution(true, "clipboard", shellName)
} else {
telemetryService.captureTerminalExecution(false, "none")
telemetryService.captureTerminalExecution(false, "none", shellName)
}
} else {
// Shell integration worked
telemetryService.captureTerminalExecution(true, "shell_integration")
telemetryService.captureTerminalExecution(true, "shell_integration", shellName)
}
// for now we don't want this delaying requests since we don't send diagnostics automatically anymore (previous: "even though the command is finished, we still want to consider it 'hot' in case so that api request stalls to let diagnostics catch up")
@@ -217,7 +285,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
this.emit("continue")
} else {
// no shell integration detected, we'll fallback to running the command and capturing the terminal's output after some time
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.NO_SHELL_INTEGRATION)
telemetryService.captureTerminalOutputFailure(TerminalOutputFailureReason.NO_SHELL_INTEGRATION, shellName)
terminal.sendText(command, true)
// wait 3 seconds for the command to run
@@ -228,9 +296,9 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
// Check if clipboard fallback worked
const terminalSnapshot = await getLatestTerminalOutput()
if (terminalSnapshot && terminalSnapshot.trim()) {
telemetryService.captureTerminalExecution(true, "clipboard")
telemetryService.captureTerminalExecution(true, "clipboard", shellName)
} else {
telemetryService.captureTerminalExecution(false, "none")
telemetryService.captureTerminalExecution(false, "none", shellName)
}
// For terminals without shell integration, we can't know when the command completes
// So we'll just emit the continue event after a delay
+9 -3
View File
@@ -1112,13 +1112,15 @@ export class TelemetryService {
* Records terminal command execution outcomes
* @param success Whether the command output was successfully captured
* @param method The method used to capture output ("shell_integration" | "clipboard" | "none")
* @param shell The shell being used (e.g., "bash", "zsh", "powershell")
*/
public captureTerminalExecution(success: boolean, method: "shell_integration" | "clipboard" | "none") {
public captureTerminalExecution(success: boolean, method: "shell_integration" | "clipboard" | "none", shell?: string) {
this.capture({
event: TelemetryService.EVENTS.TASK.TERMINAL_EXECUTION,
properties: {
success,
method,
shell,
},
})
}
@@ -1126,12 +1128,14 @@ export class TelemetryService {
/**
* Records when terminal output capture fails
* @param reason The reason for failure
* @param shell The shell being used (e.g., "bash", "zsh", "powershell")
*/
public captureTerminalOutputFailure(reason: TerminalOutputFailureReason) {
public captureTerminalOutputFailure(reason: TerminalOutputFailureReason, shell?: string) {
this.capture({
event: TelemetryService.EVENTS.TASK.TERMINAL_OUTPUT_FAILURE,
properties: {
reason,
shell,
},
})
}
@@ -1139,12 +1143,14 @@ export class TelemetryService {
/**
* Records when user has to intervene with terminal execution
* @param action The user action
* @param shell The shell being used (e.g., "bash", "zsh", "powershell")
*/
public captureTerminalUserIntervention(action: TerminalUserInterventionAction) {
public captureTerminalUserIntervention(action: TerminalUserInterventionAction, shell?: string) {
this.capture({
event: TelemetryService.EVENTS.TASK.TERMINAL_USER_INTERVENTION,
properties: {
action,
shell,
},
})
}