mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 21:01:06 -05:00
Add terminal output and diagnostics to relevant details
This commit is contained in:
307
src/ClaudeDev.ts
307
src/ClaudeDev.ts
@@ -21,7 +21,7 @@ import { getApiMetrics } from "./shared/getApiMetrics"
|
||||
import { HistoryItem } from "./shared/HistoryItem"
|
||||
import { Tool, ToolName } from "./shared/Tool"
|
||||
import { ClaudeAskResponse } from "./shared/WebviewMessage"
|
||||
import { findLast, findLastIndex } from "./utils"
|
||||
import { findLast, findLastIndex, formatContentBlockToMarkdown } from "./utils"
|
||||
import { truncateHalfConversation } from "./utils/context-management"
|
||||
import { regexSearchFiles } from "./utils/ripgrep"
|
||||
import { extractTextFromFile } from "./utils/extract-text"
|
||||
@@ -281,7 +281,7 @@ export class ClaudeDev {
|
||||
) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.api = buildApiHandler(apiConfiguration)
|
||||
this.terminalManager = new TerminalManager(provider.context)
|
||||
this.terminalManager = new TerminalManager()
|
||||
this.customInstructions = customInstructions
|
||||
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
||||
|
||||
@@ -445,31 +445,6 @@ export class ClaudeDev {
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
}
|
||||
|
||||
private formatImagesIntoBlocks(images?: string[]): Anthropic.ImageBlockParam[] {
|
||||
return images
|
||||
? images.map((dataUrl) => {
|
||||
// data:image/png;base64,base64string
|
||||
const [rest, base64] = dataUrl.split(",")
|
||||
const mimeType = rest.split(":")[1].split(";")[0]
|
||||
return {
|
||||
type: "image",
|
||||
source: { type: "base64", media_type: mimeType, data: base64 },
|
||||
} as Anthropic.ImageBlockParam
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
private formatIntoToolResponse(text: string, images?: string[]): ToolResponse {
|
||||
if (images && images.length > 0) {
|
||||
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
|
||||
const imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
|
||||
// Placing images after text leads to better results
|
||||
return [textBlock, ...imageBlocks]
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
private async startTask(task?: string, images?: string[]): Promise<void> {
|
||||
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync
|
||||
// if the extension process were killed, then on restart the claudeMessages might not be empty, so we need to set it to [] when we create a new ClaudeDev client (otherwise webview would show stale messages from previous session)
|
||||
@@ -486,21 +461,19 @@ export class ClaudeDev {
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
JSON.stringify({
|
||||
request: this.api.createUserReadableRequest([
|
||||
{
|
||||
type: "text",
|
||||
text: `${taskText}\n\n<potentially_relevant_details>(see getPotentiallyRelevantDetails in src/ClaudeDev.ts)</potentially_relevant_details>`,
|
||||
},
|
||||
...imageBlocks,
|
||||
]),
|
||||
request: `${taskText}\n\n<potentially_relevant_details>\nLoading...\n</potentially_relevant_details>`,
|
||||
})
|
||||
)
|
||||
this.shouldSkipNextApiReqStartedMessage = true
|
||||
this.getPotentiallyRelevantDetails(true).then(async (verboseDetails) => {
|
||||
this.getInitialDetails().then(async (initialDetails) => {
|
||||
const lastApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started")
|
||||
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({ request: `${taskText}\n\n${initialDetails}` })
|
||||
await this.saveClaudeMessages()
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
await this.initiateTaskLoop([
|
||||
{
|
||||
type: "text",
|
||||
text: `${taskText}\n\n${verboseDetails}`, // cannot be sent with system prompt since it's cached and these details can change
|
||||
text: `${taskText}\n\n${initialDetails}`, // cannot be sent with system prompt since it's cached and these details can change
|
||||
},
|
||||
...imageBlocks,
|
||||
])
|
||||
@@ -687,8 +660,7 @@ export class ClaudeDev {
|
||||
: "") +
|
||||
(newUserContentText
|
||||
? `\n\nNew instructions for task continuation:\n<user_message>\n${newUserContentText}\n</user_message>\n`
|
||||
: "") +
|
||||
`\n\n${await this.getPotentiallyRelevantDetails()}`
|
||||
: "")
|
||||
|
||||
const newUserContentImages = newUserContent.filter((block) => block.type === "image")
|
||||
const combinedModifiedOldUserContentWithNewUserContent: UserContent = (
|
||||
@@ -781,21 +753,12 @@ export class ClaudeDev {
|
||||
|
||||
async writeToFile(relPath?: string, newContent?: string): Promise<ToolResponse> {
|
||||
if (relPath === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use write_to_file without value for required parameter 'path'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("write_to_file", "path")
|
||||
}
|
||||
|
||||
if (newContent === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
`Claude tried to use write_to_file for '${relPath}' without value for required parameter 'content'. This is likely due to output token limits. Retrying...`
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'content'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("write_to_file", "content", relPath)
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
try {
|
||||
@@ -991,9 +954,9 @@ export class ClaudeDev {
|
||||
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
|
||||
const editedContent = updatedDocument.getText()
|
||||
@@ -1070,9 +1033,11 @@ export class ClaudeDev {
|
||||
diff: this.createPrettyPatch(relPath, normalizedNewContent, normalizedEditedContent),
|
||||
} as ClaudeSayTool)
|
||||
)
|
||||
return `The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath}. Note this does not mean you need to re-write the file with the user's changes, they have already been applied to the file.`
|
||||
return this.formatToolResult(
|
||||
`The user made the following updates to your content:\n\n${userDiff}\n\nThe updated content, which includes both your original modifications and the user's additional edits, has been successfully saved to ${relPath}. Note this does not mean you need to re-write the file with the user's changes, they have already been applied to the file.`
|
||||
)
|
||||
} else {
|
||||
return `The content was successfully saved to ${relPath}.`
|
||||
return this.formatToolResult(`The content was successfully saved to ${relPath}.`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
||||
@@ -1080,7 +1045,7 @@ export class ClaudeDev {
|
||||
"error",
|
||||
`Error writing file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
||||
)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1152,12 +1117,8 @@ export class ClaudeDev {
|
||||
|
||||
async readFile(relPath?: string): Promise<ToolResponse> {
|
||||
if (relPath === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use read_file without value for required parameter 'path'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("read_file", "path")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
try {
|
||||
@@ -1176,9 +1137,9 @@ export class ClaudeDev {
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1189,18 +1150,14 @@ export class ClaudeDev {
|
||||
"error",
|
||||
`Error reading file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
||||
)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles(relDirPath?: string, recursiveRaw?: string): Promise<ToolResponse> {
|
||||
if (relDirPath === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use list_files without value for required parameter 'path'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("list_files", "path")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
try {
|
||||
@@ -1221,13 +1178,13 @@ export class ClaudeDev {
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return this.formatToolResult(result)
|
||||
} catch (error) {
|
||||
const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}`
|
||||
await this.say(
|
||||
@@ -1236,7 +1193,7 @@ export class ClaudeDev {
|
||||
error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
}`
|
||||
)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,12 +1258,8 @@ export class ClaudeDev {
|
||||
|
||||
async listCodeDefinitionNames(relDirPath?: string): Promise<ToolResponse> {
|
||||
if (relDirPath === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use list_code_definition_names without value for required parameter 'path'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("list_code_definition_names", "path")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
try {
|
||||
@@ -1325,13 +1278,13 @@ export class ClaudeDev {
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return this.formatToolResult(result)
|
||||
} catch (error) {
|
||||
const errorString = `Error parsing source code definitions: ${JSON.stringify(serializeError(error))}`
|
||||
await this.say(
|
||||
@@ -1340,26 +1293,18 @@ export class ClaudeDev {
|
||||
error.message ?? JSON.stringify(serializeError(error), null, 2)
|
||||
}`
|
||||
)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
async searchFiles(relDirPath: string, regex: string, filePattern?: string): Promise<ToolResponse> {
|
||||
if (relDirPath === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use search_files without value for required parameter 'path'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'path'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("search_files", "path")
|
||||
}
|
||||
if (regex === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
`Claude tried to use search_files without value for required parameter 'regex'. Retrying...`
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'regex'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("search_files", "regex", relDirPath)
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
try {
|
||||
@@ -1381,40 +1326,36 @@ export class ClaudeDev {
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
return this.formatToolResult(results)
|
||||
} catch (error) {
|
||||
const errorString = `Error searching files: ${JSON.stringify(serializeError(error))}`
|
||||
await this.say(
|
||||
"error",
|
||||
`Error searching files:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
||||
)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
async executeCommand(command?: string, returnEmptyStringOnSuccess: boolean = false): Promise<ToolResponse> {
|
||||
if (command === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use execute_command without value for required parameter 'command'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'command'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("execute_command", "command")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const { response, text, images } = await this.ask("command", command)
|
||||
if (response !== "yesButtonTapped") {
|
||||
if (response === "messageResponse") {
|
||||
await this.say("user_feedback", text, images)
|
||||
return this.formatIntoToolResponse(await this.formatGenericToolFeedback(text), images)
|
||||
return this.formatToolResponseWithImages(await this.formatToolDeniedFeedback(text), images)
|
||||
}
|
||||
return "The user denied this operation."
|
||||
return await this.formatToolDenied()
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1439,7 +1380,7 @@ export class ClaudeDev {
|
||||
|
||||
let result = ""
|
||||
process.on("line", (line) => {
|
||||
console.log("sending line from here", line)
|
||||
console.log("New line from process:", line)
|
||||
result += line
|
||||
sendCommandOutput(line)
|
||||
})
|
||||
@@ -1455,10 +1396,8 @@ export class ClaudeDev {
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||
return this.formatIntoToolResponse(
|
||||
`Command Output:\n${result}\n\nThe user interrupted the command and provided the following feedback:\n<feedback>\n${
|
||||
userFeedback.text
|
||||
}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`,
|
||||
return this.formatToolResponseWithImages(
|
||||
`Command Output:\n${result}\n\nThe user interrupted the command and provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
|
||||
userFeedback.images
|
||||
)
|
||||
}
|
||||
@@ -1467,39 +1406,31 @@ export class ClaudeDev {
|
||||
if (returnEmptyStringOnSuccess) {
|
||||
return ""
|
||||
}
|
||||
return `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`
|
||||
return await this.formatToolResult(`Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`)
|
||||
} catch (error) {
|
||||
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
|
||||
const errorString = `Error executing command:\n${errorMessage}`
|
||||
await this.say("error", `Error executing command:\n${errorMessage}`)
|
||||
return errorString
|
||||
return await this.formatToolError(errorString)
|
||||
}
|
||||
}
|
||||
|
||||
async askFollowupQuestion(question?: string): Promise<ToolResponse> {
|
||||
if (question === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use ask_followup_question without value for required parameter 'question'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'question'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("ask_followup_question", "question")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
const { text, images } = await this.ask("followup", question)
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
return this.formatIntoToolResponse(`<answer>\n${text}\n</answer>`, images)
|
||||
return this.formatToolResponseWithImages(`<answer>\n${text}\n</answer>`, images)
|
||||
}
|
||||
|
||||
async attemptCompletion(result?: string, command?: string): Promise<ToolResponse> {
|
||||
// result is required, command is optional
|
||||
if (result === undefined) {
|
||||
await this.say(
|
||||
"error",
|
||||
"Claude tried to use attempt_completion without value for required parameter 'result'. Retrying..."
|
||||
)
|
||||
this.consecutiveMistakeCount++
|
||||
return "Error: Missing value for required parameter 'result'. Please retry with complete response."
|
||||
return await this.sayAndCreateMissingParamError("attempt_completion", "result")
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
let resultToSend = result
|
||||
@@ -1518,8 +1449,8 @@ export class ClaudeDev {
|
||||
return "" // signals to recursive loop to stop (for now this never happens since yesButtonTapped will trigger a new task)
|
||||
}
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
return this.formatIntoToolResponse(
|
||||
`The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`,
|
||||
return this.formatToolResponseWithImages(
|
||||
`The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
|
||||
images
|
||||
)
|
||||
}
|
||||
@@ -1600,7 +1531,7 @@ ${this.customInstructions.trim()}
|
||||
...[
|
||||
{
|
||||
type: "text",
|
||||
text: `You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${text}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`,
|
||||
text: `You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n<feedback>\n${text}\n</feedback>`,
|
||||
} as Anthropic.Messages.TextBlockParam,
|
||||
...this.formatImagesIntoBlocks(images),
|
||||
]
|
||||
@@ -1609,15 +1540,15 @@ ${this.customInstructions.trim()}
|
||||
this.consecutiveMistakeCount = 0
|
||||
}
|
||||
|
||||
// add potentially relevant details as its own text block, separate from tool results
|
||||
userContent.push({ type: "text", text: await this.getPotentiallyRelevantDetails() })
|
||||
|
||||
await this.addToApiConversationHistory({ role: "user", content: userContent })
|
||||
|
||||
if (!this.shouldSkipNextApiReqStartedMessage) {
|
||||
await this.say(
|
||||
"api_req_started",
|
||||
// what the user sees in the webview
|
||||
JSON.stringify({
|
||||
request: this.api.createUserReadableRequest(userContent),
|
||||
})
|
||||
JSON.stringify({ request: userContent.map(formatContentBlockToMarkdown).join("\n\n") })
|
||||
)
|
||||
} else {
|
||||
this.shouldSkipNextApiReqStartedMessage = false
|
||||
@@ -1748,9 +1679,50 @@ ${this.customInstructions.trim()}
|
||||
}
|
||||
}
|
||||
|
||||
// Prompts
|
||||
// Formatting responses to Claude
|
||||
|
||||
async getPotentiallyRelevantDetails(verbose: boolean = false) {
|
||||
private formatImagesIntoBlocks(images?: string[]): Anthropic.ImageBlockParam[] {
|
||||
return images
|
||||
? images.map((dataUrl) => {
|
||||
// data:image/png;base64,base64string
|
||||
const [rest, base64] = dataUrl.split(",")
|
||||
const mimeType = rest.split(":")[1].split(";")[0]
|
||||
return {
|
||||
type: "image",
|
||||
source: { type: "base64", media_type: mimeType, data: base64 },
|
||||
} as Anthropic.ImageBlockParam
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
private formatToolResponseWithImages(text: string, images?: string[]): ToolResponse {
|
||||
if (images && images.length > 0) {
|
||||
const textBlock: Anthropic.TextBlockParam = { type: "text", text }
|
||||
const imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
|
||||
// Placing images after text leads to better results
|
||||
return [textBlock, ...imageBlocks]
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
async getInitialDetails() {
|
||||
let details = "<potentially_relevant_details>"
|
||||
|
||||
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
||||
const files = await listFiles(cwd, !isDesktop)
|
||||
const result = this.formatFilesList(cwd, files)
|
||||
details += `\n# Current Working Directory ('${cwd}') File Structure:${
|
||||
isDesktop
|
||||
? "\n(Desktop so only top-level contents shown for brevity, use list_files to explore further if necessary)"
|
||||
: ""
|
||||
}\n${result}\n`
|
||||
|
||||
details += "</potentially_relevant_details>"
|
||||
return details
|
||||
}
|
||||
|
||||
async getPotentiallyRelevantDetails() {
|
||||
let details = `<potentially_relevant_details>
|
||||
# VSCode Visible Files:
|
||||
${
|
||||
@@ -1769,25 +1741,76 @@ ${
|
||||
.filter(Boolean)
|
||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||
.join("\n") || "(No tabs open)"
|
||||
}
|
||||
`
|
||||
}`
|
||||
|
||||
if (verbose) {
|
||||
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
||||
const files = await listFiles(cwd, !isDesktop)
|
||||
const result = this.formatFilesList(cwd, files)
|
||||
details += `\n# Current Working Directory ('${cwd}') File Structure:${
|
||||
isDesktop
|
||||
? "\n(Desktop so only top-level contents shown for brevity, use list_files to explore further if necessary)"
|
||||
: ""
|
||||
}:\n${result}\n`
|
||||
const busyTerminals = this.terminalManager.getBusyTerminals()
|
||||
if (busyTerminals.length > 0) {
|
||||
details += "\n\n# Active Terminals:"
|
||||
for (const busyTerminal of busyTerminals) {
|
||||
details += `\n## Original command:\n${busyTerminal.lastCommand}`
|
||||
const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
|
||||
if (newOutput) {
|
||||
details += `\n## New output since last check:\n${newOutput}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details += "</potentially_relevant_details>"
|
||||
// Get diagnostics for all open files in the workspace
|
||||
const diagnostics = vscode.languages.getDiagnostics()
|
||||
const relevantDiagnostics = diagnostics.filter(([_, fileDiagnostics]) =>
|
||||
fileDiagnostics.some(
|
||||
(d) =>
|
||||
d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning
|
||||
)
|
||||
)
|
||||
|
||||
if (relevantDiagnostics.length > 0) {
|
||||
details += "\n\n# Workspace Diagnostics:"
|
||||
for (const [uri, fileDiagnostics] of relevantDiagnostics) {
|
||||
const relativePath = path.relative(cwd, uri.fsPath)
|
||||
details += `\n## ${relativePath}:`
|
||||
for (const diagnostic of fileDiagnostics) {
|
||||
if (
|
||||
diagnostic.severity === vscode.DiagnosticSeverity.Error ||
|
||||
diagnostic.severity === vscode.DiagnosticSeverity.Warning
|
||||
) {
|
||||
let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
|
||||
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
|
||||
details += `\n- [${severity}] Line ${line}: ${diagnostic.message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
details += "\n</potentially_relevant_details>"
|
||||
return details
|
||||
}
|
||||
|
||||
async formatGenericToolFeedback(feedback?: string) {
|
||||
return `The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`
|
||||
async formatToolDeniedFeedback(feedback?: string) {
|
||||
return `The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>`
|
||||
}
|
||||
|
||||
async formatToolDenied() {
|
||||
return `The user denied this operation.`
|
||||
}
|
||||
|
||||
async formatToolResult(result: string) {
|
||||
return result // the successful result of the tool should never be manipulated, if we need to add details it should be as a separate user text block
|
||||
}
|
||||
|
||||
async formatToolError(error?: string) {
|
||||
return `The tool execution failed with the following error:\n<error>\n${error}\n</error>`
|
||||
}
|
||||
|
||||
async sayAndCreateMissingParamError(toolName: ToolName, paramName: string, relPath?: string) {
|
||||
await this.say(
|
||||
"error",
|
||||
`Claude tried to use ${toolName}${
|
||||
relPath ? ` for '${relPath}'` : ""
|
||||
} without value for required parameter '${paramName}'. Retrying...`
|
||||
)
|
||||
return await this.formatToolError(
|
||||
`Missing value for required parameter '${paramName}'. Please retry with complete response.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../shared/api"
|
||||
|
||||
export class AnthropicHandler implements ApiHandler {
|
||||
@@ -99,24 +99,6 @@ export class AnthropicHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: AnthropicModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in anthropicModels) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../shared/api"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||
@@ -39,24 +39,6 @@ export class AwsBedrockHandler implements ApiHandler {
|
||||
return { message }
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: BedrockModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in bedrockModels) {
|
||||
|
||||
@@ -19,15 +19,6 @@ export interface ApiHandler {
|
||||
tools: Anthropic.Messages.Tool[]
|
||||
): Promise<ApiHandlerMessageResponse>
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any
|
||||
|
||||
getModel(): { id: string; info: ModelInfo }
|
||||
}
|
||||
|
||||
@@ -50,31 +41,3 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new AnthropicHandler(options)
|
||||
}
|
||||
}
|
||||
|
||||
export function withoutImageData(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): Array<
|
||||
Anthropic.TextBlockParam | Anthropic.ImageBlockParam | Anthropic.ToolUseBlockParam | Anthropic.ToolResultBlockParam
|
||||
> {
|
||||
return userContent.map((part) => {
|
||||
if (part.type === "image") {
|
||||
return { ...part, source: { ...part.source, data: "..." } }
|
||||
} else if (part.type === "tool_result" && typeof part.content !== "string") {
|
||||
return {
|
||||
...part,
|
||||
content: part.content?.map((contentPart) => {
|
||||
if (contentPart.type === "image") {
|
||||
return { ...contentPart, source: { ...contentPart.source, data: "..." } }
|
||||
}
|
||||
return contentPart
|
||||
}),
|
||||
}
|
||||
}
|
||||
return part
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
||||
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
||||
|
||||
@@ -48,23 +48,6 @@ export class OllamaHandler implements ApiHandler {
|
||||
return { message: anthropicMessage }
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.options.ollamaModelId ?? "",
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.ollamaModelId ?? "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
||||
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
||||
|
||||
@@ -48,23 +48,6 @@ export class OpenAiHandler implements ApiHandler {
|
||||
return { message: anthropicMessage }
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.options.openAiModelId ?? "",
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
return {
|
||||
id: this.options.openAiModelId ?? "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import {
|
||||
ApiHandlerOptions,
|
||||
ModelInfo,
|
||||
@@ -177,24 +177,6 @@ export class OpenRouterHandler implements ApiHandler {
|
||||
return completion
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: OpenRouterModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in openRouterModels) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
||||
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../shared/api"
|
||||
|
||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||
@@ -33,24 +33,6 @@ export class VertexHandler implements ApiHandler {
|
||||
return { message }
|
||||
}
|
||||
|
||||
createUserReadableRequest(
|
||||
userContent: Array<
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
| Anthropic.ToolUseBlockParam
|
||||
| Anthropic.ToolResultBlockParam
|
||||
>
|
||||
): any {
|
||||
return {
|
||||
model: this.getModel().id,
|
||||
max_tokens: this.getModel().info.maxTokens,
|
||||
system: "(see SYSTEM_PROMPT in src/ClaudeDev.ts)",
|
||||
messages: [{ conversation_history: "..." }, { role: "user", content: withoutImageData(userContent) }],
|
||||
tools: "(see tools in src/ClaudeDev.ts)",
|
||||
tool_choice: { type: "auto" },
|
||||
}
|
||||
}
|
||||
|
||||
getModel(): { id: VertexModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in vertexModels) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as vscode from "vscode"
|
||||
import { EventEmitter } from "events"
|
||||
import delay from "delay"
|
||||
import pWaitFor from "p-wait-for"
|
||||
|
||||
/*
|
||||
TerminalManager:
|
||||
@@ -21,6 +21,14 @@ Enables flexible command execution:
|
||||
- Continue execution in background
|
||||
- Retrieve missed output later
|
||||
|
||||
Notes:
|
||||
- it turns out some shellIntegration APIs are available on cursor, although not on older versions of vscode
|
||||
- "By default, the shell integration script should automatically activate on supported shells launched from VS Code."
|
||||
Supported shells:
|
||||
Linux/macOS: bash, fish, pwsh, zsh
|
||||
Windows: pwsh
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
const terminalManager = new TerminalManager(context);
|
||||
@@ -41,77 +49,75 @@ process.continue();
|
||||
// Later, if you need to get the unretrieved output:
|
||||
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
|
||||
console.log('Unretrieved output:', unretrievedOutput);
|
||||
|
||||
Resources:
|
||||
- https://github.com/microsoft/vscode/issues/226655
|
||||
- https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api
|
||||
- https://code.visualstudio.com/docs/terminal/shell-integration
|
||||
- https://code.visualstudio.com/api/references/vscode-api#Terminal
|
||||
- https://github.com/microsoft/vscode-extension-samples/blob/main/terminal-sample/src/extension.ts
|
||||
- https://github.com/microsoft/vscode-extension-samples/blob/main/shell-integration-sample/src/extension.ts
|
||||
*/
|
||||
|
||||
export class TerminalManager {
|
||||
private terminals: TerminalInfo[] = []
|
||||
private processes: Map<number, TerminalProcess> = new Map()
|
||||
private context: vscode.ExtensionContext
|
||||
private nextTerminalId = 1
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.context = context
|
||||
this.setupListeners()
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
// todo: make sure we do this check everywhere we use the new terminal APIs
|
||||
if (hasShellIntegrationApis()) {
|
||||
this.context.subscriptions.push(
|
||||
vscode.window.onDidOpenTerminal(this.handleOpenTerminal.bind(this)),
|
||||
vscode.window.onDidCloseTerminal(this.handleClosedTerminal.bind(this)),
|
||||
vscode.window.onDidChangeTerminalShellIntegration(this.handleShellIntegrationChange.bind(this)),
|
||||
vscode.window.onDidStartTerminalShellExecution(this.handleShellExecutionStart.bind(this)),
|
||||
vscode.window.onDidEndTerminalShellExecution(this.handleShellExecutionEnd.bind(this))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
runCommand(terminalInfo: TerminalInfo, command: string, cwd: string): TerminalProcessResultPromise {
|
||||
terminalInfo.busy = true
|
||||
terminalInfo.lastCommand = command
|
||||
|
||||
const process = new TerminalProcess(terminalInfo, command)
|
||||
|
||||
const process = new TerminalProcess()
|
||||
this.processes.set(terminalInfo.id, process)
|
||||
|
||||
process.once("completed", () => {
|
||||
console.log(`completed received for terminal ${terminalInfo.id}`)
|
||||
terminalInfo.busy = false
|
||||
})
|
||||
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
process.once(CONTINUE_EVENT, () => {
|
||||
console.log("2")
|
||||
process.once("continue", () => {
|
||||
console.log(`continue received for terminal ${terminalInfo.id}`)
|
||||
resolve()
|
||||
})
|
||||
process.once("error", reject)
|
||||
process.once("error", (error) => {
|
||||
console.error(`Error in terminal ${terminalInfo.id}:`, error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
// if shell integration is already active, run the command immediately
|
||||
if (terminalInfo.terminal.shellIntegration) {
|
||||
console.log(`Shell integration active for terminal ${terminalInfo.id}, running command immediately`)
|
||||
process.waitForShellIntegration = false
|
||||
process.run()
|
||||
}
|
||||
|
||||
if (hasShellIntegrationApis()) {
|
||||
// Fallback to sendText if there is no shell integration within 3 seconds of launching (could be because the user is not running one of the supported shells)
|
||||
setTimeout(() => {
|
||||
if (!terminalInfo.terminal.shellIntegration) {
|
||||
process.waitForShellIntegration = false
|
||||
process.run()
|
||||
// Without shell integration, we can't know when the command has finished or what the
|
||||
// exit code was.
|
||||
}
|
||||
}, 3000)
|
||||
process.run(terminalInfo.terminal, command)
|
||||
} else {
|
||||
// User doesn't have shell integration API available, run command the old way
|
||||
process.waitForShellIntegration = false
|
||||
process.run()
|
||||
console.log(`Waiting for shell integration for terminal ${terminalInfo.id}`)
|
||||
// docs recommend waiting 3s for shell integration to activate
|
||||
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
|
||||
console.log(
|
||||
`Shell integration ${
|
||||
terminalInfo.terminal.shellIntegration ? "activated" : "not activated"
|
||||
} for terminal ${terminalInfo.id}`
|
||||
)
|
||||
|
||||
const existingProcess = this.processes.get(terminalInfo.id)
|
||||
if (existingProcess && existingProcess.waitForShellIntegration) {
|
||||
existingProcess.waitForShellIntegration = false
|
||||
existingProcess.run(terminalInfo.terminal, command)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Merge the process and promise
|
||||
return mergePromise(process, promise)
|
||||
}
|
||||
|
||||
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
||||
const availableTerminal = this.terminals.find((t) => {
|
||||
if (t.busy) {
|
||||
// it seems even if you close the terminal, it can still be reused
|
||||
const isDisposed = !t.terminal || t.terminal.exitStatus // The exit status of the terminal will be undefined while the terminal is active.
|
||||
console.log(`Terminal ${t.id} isDisposed:`, isDisposed)
|
||||
if (t.busy || isDisposed) {
|
||||
return false
|
||||
}
|
||||
const terminalCwd = t.terminal.shellIntegration?.cwd // one of claude's commands could have changed the cwd of the terminal
|
||||
@@ -121,7 +127,7 @@ export class TerminalManager {
|
||||
return vscode.Uri.file(cwd).fsPath === terminalCwd.fsPath
|
||||
})
|
||||
if (availableTerminal) {
|
||||
console.log("reusing terminal", availableTerminal.id)
|
||||
console.log("Reusing terminal", availableTerminal.id)
|
||||
return availableTerminal
|
||||
}
|
||||
|
||||
@@ -140,63 +146,10 @@ export class TerminalManager {
|
||||
return newTerminalInfo
|
||||
}
|
||||
|
||||
private handleOpenTerminal(terminal: vscode.Terminal) {
|
||||
console.log(`Terminal opened: ${terminal.name}`)
|
||||
}
|
||||
|
||||
private handleClosedTerminal(terminal: vscode.Terminal) {
|
||||
const index = this.terminals.findIndex((t) => t.terminal === terminal)
|
||||
if (index !== -1) {
|
||||
const terminalInfo = this.terminals[index]
|
||||
this.terminals.splice(index, 1)
|
||||
this.processes.delete(terminalInfo.id)
|
||||
}
|
||||
console.log(`Terminal closed: ${terminal.name}`)
|
||||
}
|
||||
|
||||
private handleShellIntegrationChange(e: vscode.TerminalShellIntegrationChangeEvent) {
|
||||
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||
if (terminalInfo) {
|
||||
const process = this.processes.get(terminalInfo.id)
|
||||
if (process && process.waitForShellIntegration) {
|
||||
process.waitForShellIntegration = false
|
||||
process.run()
|
||||
}
|
||||
console.log(`Shell integration activated for terminal: ${e.terminal.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
private handleShellExecutionStart(e: vscode.TerminalShellExecutionStartEvent) {
|
||||
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||
if (terminalInfo) {
|
||||
terminalInfo.busy = true
|
||||
terminalInfo.lastCommand = e.execution.commandLine.value
|
||||
console.log(`Command started in terminal ${terminalInfo.id}: ${terminalInfo.lastCommand}`)
|
||||
}
|
||||
}
|
||||
|
||||
private handleShellExecutionEnd(e: vscode.TerminalShellExecutionEndEvent) {
|
||||
const terminalInfo = this.terminals.find((t) => t.terminal === e.terminal)
|
||||
if (terminalInfo) {
|
||||
this.handleCommandCompletion(terminalInfo, e.exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
private handleCommandCompletion(terminalInfo: TerminalInfo, exitCode?: number | undefined) {
|
||||
terminalInfo.busy = false
|
||||
console.log(
|
||||
`Command "${terminalInfo.lastCommand}" in terminal ${terminalInfo.id} completed with exit code: ${exitCode}`
|
||||
)
|
||||
}
|
||||
|
||||
getBusyTerminals(): { id: number; lastCommand: string }[] {
|
||||
return this.terminals.filter((t) => t.busy).map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
||||
}
|
||||
|
||||
hasBusyTerminals(): boolean {
|
||||
return this.terminals.some((t) => t.busy)
|
||||
}
|
||||
|
||||
getUnretrievedOutput(terminalId: number): string {
|
||||
const process = this.processes.get(terminalId)
|
||||
if (!process) {
|
||||
@@ -206,19 +159,14 @@ export class TerminalManager {
|
||||
}
|
||||
|
||||
disposeAll() {
|
||||
for (const info of this.terminals) {
|
||||
info.terminal.dispose() // todo do we want to do this? test with tab view closing it
|
||||
}
|
||||
// for (const info of this.terminals) {
|
||||
// //info.terminal.dispose() // dont want to dispose terminals when task is aborted
|
||||
// }
|
||||
this.terminals = []
|
||||
this.processes.clear()
|
||||
}
|
||||
}
|
||||
|
||||
function hasShellIntegrationApis(): boolean {
|
||||
const [major, minor] = vscode.version.split(".").map(Number)
|
||||
return major > 1 || (major === 1 && minor >= 93)
|
||||
}
|
||||
|
||||
interface TerminalInfo {
|
||||
terminal: vscode.Terminal
|
||||
busy: boolean
|
||||
@@ -226,54 +174,59 @@ interface TerminalInfo {
|
||||
id: number
|
||||
}
|
||||
|
||||
const CONTINUE_EVENT = "CONTINUE_EVENT"
|
||||
interface TerminalProcessEvents {
|
||||
line: [line: string]
|
||||
continue: []
|
||||
completed: []
|
||||
error: [error: Error]
|
||||
}
|
||||
|
||||
export class TerminalProcess extends EventEmitter {
|
||||
export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
|
||||
waitForShellIntegration: boolean = true
|
||||
private isListening: boolean = true
|
||||
private buffer: string = ""
|
||||
private execution?: vscode.TerminalShellExecution
|
||||
private stream?: AsyncIterable<string>
|
||||
private fullOutput: string = ""
|
||||
private lastRetrievedIndex: number = 0
|
||||
|
||||
constructor(public terminalInfo: TerminalInfo, private command: string) {
|
||||
super()
|
||||
}
|
||||
// constructor() {
|
||||
// super()
|
||||
|
||||
async run() {
|
||||
if (this.terminalInfo.terminal.shellIntegration) {
|
||||
this.execution = this.terminalInfo.terminal.shellIntegration.executeCommand(this.command)
|
||||
this.stream = this.execution.read()
|
||||
async run(terminal: vscode.Terminal, command: string) {
|
||||
if (terminal.shellIntegration) {
|
||||
console.log(`Shell integration available for terminal`)
|
||||
const execution = terminal.shellIntegration.executeCommand(command)
|
||||
const stream = execution.read()
|
||||
// todo: need to handle errors
|
||||
let isFirstChunk = true // ignore first chunk since it's vscode shell integration marker
|
||||
for await (const data of this.stream) {
|
||||
console.log("data", data)
|
||||
if (!isFirstChunk) {
|
||||
this.fullOutput += data
|
||||
if (this.isListening) {
|
||||
this.emitIfEol(data)
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
}
|
||||
} else {
|
||||
isFirstChunk = false
|
||||
for await (const data of stream) {
|
||||
console.log(`Received data chunk for terminal:`, data)
|
||||
this.fullOutput += data
|
||||
if (this.isListening) {
|
||||
console.log(`Emitting data for terminal`)
|
||||
this.emitIfEol(data)
|
||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||
}
|
||||
}
|
||||
|
||||
// Emit any remaining content in the buffer
|
||||
if (this.buffer && this.isListening) {
|
||||
console.log(`Emitting remaining buffer for terminal:`, this.buffer.trim())
|
||||
this.emit("line", this.buffer.trim())
|
||||
this.buffer = ""
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
}
|
||||
|
||||
this.emit(CONTINUE_EVENT)
|
||||
console.log(`Command execution completed for terminal`)
|
||||
this.emit("continue")
|
||||
this.emit("completed")
|
||||
} else {
|
||||
this.terminalInfo.terminal.sendText(this.command, true)
|
||||
console.log(`Shell integration not available for terminal, falling back to sendText`)
|
||||
terminal.sendText(command, true)
|
||||
// For terminals without shell integration, we can't know when the command completes
|
||||
// So we'll just emit the continue event after a delay
|
||||
setTimeout(() => {
|
||||
this.emit(CONTINUE_EVENT)
|
||||
console.log(`Emitting continue after delay for terminal`)
|
||||
this.emit("continue")
|
||||
// can't emit completed since we don't if the command actually completed, it could still be running server
|
||||
}, 2000) // Adjust this delay as needed
|
||||
}
|
||||
}
|
||||
@@ -294,13 +247,17 @@ export class TerminalProcess extends EventEmitter {
|
||||
}
|
||||
|
||||
continue() {
|
||||
// Emit any remaining content in the buffer
|
||||
if (this.buffer && this.isListening) {
|
||||
console.log(`Emitting remaining buffer for terminal:`, this.buffer.trim())
|
||||
this.emit("line", this.buffer.trim())
|
||||
this.buffer = ""
|
||||
this.lastRetrievedIndex = this.fullOutput.length
|
||||
}
|
||||
|
||||
this.isListening = false
|
||||
this.removeAllListeners("line")
|
||||
this.emit(CONTINUE_EVENT)
|
||||
}
|
||||
|
||||
isStillListening() {
|
||||
return this.isListening
|
||||
this.emit("continue")
|
||||
}
|
||||
|
||||
getUnretrievedOutput(): string {
|
||||
|
||||
@@ -4,7 +4,7 @@ import os from "os"
|
||||
import * as path from "path"
|
||||
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
||||
|
||||
export const LIST_FILES_LIMIT = 500
|
||||
export const LIST_FILES_LIMIT = 200
|
||||
|
||||
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
||||
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi
|
||||
}
|
||||
}
|
||||
|
||||
function formatContentBlockToMarkdown(
|
||||
export function formatContentBlockToMarkdown(
|
||||
block:
|
||||
| Anthropic.TextBlockParam
|
||||
| Anthropic.ImageBlockParam
|
||||
|
||||
Reference in New Issue
Block a user