mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -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
|
||||
|
||||
@@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown"
|
||||
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
||||
import CodeAccordian from "./CodeAccordian"
|
||||
import CodeBlock from "./CodeBlock"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
import Thumbnails from "./Thumbnails"
|
||||
|
||||
interface ChatRowProps {
|
||||
@@ -352,8 +352,8 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa
|
||||
{isExpanded && (
|
||||
<div style={{ marginTop: "10px" }}>
|
||||
<CodeAccordian
|
||||
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
||||
language="json"
|
||||
code={JSON.parse(message.text || "{}").request}
|
||||
language="markdown"
|
||||
isExpanded={true}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
@@ -516,19 +516,32 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-sideBar-border)",
|
||||
overflow: "hidden",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} />
|
||||
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
|
||||
{output.length > 0 && (
|
||||
<div style={{ width: "100%" }}>
|
||||
<div
|
||||
onClick={onToggleExpand}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
cursor: "pointer",
|
||||
padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
|
||||
}}>
|
||||
<span
|
||||
className={`codicon codicon-chevron-${
|
||||
isExpanded ? "down" : "right"
|
||||
}`}></span>
|
||||
<span style={{ fontSize: "0.8em" }}>Command Output</span>
|
||||
</div>
|
||||
{isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{output.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
border: "1px solid var(--vscode-sideBar-border)",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
|
||||
@@ -5,7 +5,7 @@ import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||
|
||||
const BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
|
||||
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
|
||||
|
||||
/*
|
||||
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
|
||||
@@ -15,12 +15,27 @@ this fixes the issue of right padding clipped off
|
||||
minWidth: "max-content",
|
||||
*/
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
interface CodeBlockProps {
|
||||
source?: string
|
||||
forceWrap?: boolean
|
||||
}
|
||||
|
||||
const StyledMarkdown = styled.div<{ forceWrap: boolean }>`
|
||||
${({ forceWrap }) =>
|
||||
forceWrap &&
|
||||
`
|
||||
pre, code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
`}
|
||||
|
||||
pre {
|
||||
background-color: ${BG_COLOR};
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
border-radius: 5px;
|
||||
margin: 0;
|
||||
min-width: max-content;
|
||||
min-width: ${({ forceWrap }) => (forceWrap ? "auto" : "max-content")};
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
@@ -43,7 +58,7 @@ const StyledMarkdown = styled.div`
|
||||
}
|
||||
word-wrap: break-word;
|
||||
border-radius: 5px;
|
||||
background-color: ${BG_COLOR};
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
}
|
||||
@@ -53,7 +68,7 @@ const StyledMarkdown = styled.div`
|
||||
color: #f78383;
|
||||
}
|
||||
|
||||
background-color: ${BG_COLOR};
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||
@@ -84,7 +99,7 @@ const StyledPre = styled.pre<{ theme: any }>`
|
||||
.join("")}
|
||||
`
|
||||
|
||||
const CodeBlock = memo(({ source }: { source?: string }) => {
|
||||
const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
|
||||
const { theme } = useExtensionState()
|
||||
const [reactContent, setMarkdownSource] = useRemark({
|
||||
remarkPlugins: [
|
||||
@@ -121,11 +136,11 @@ const CodeBlock = memo(({ source }: { source?: string }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflowY: "auto",
|
||||
maxHeight: "100%",
|
||||
backgroundColor: BG_COLOR,
|
||||
overflowY: forceWrap ? "visible" : "auto",
|
||||
maxHeight: forceWrap ? "none" : "100%",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<StyledMarkdown>{reactContent}</StyledMarkdown>
|
||||
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -109,7 +109,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span style={{ fontWeight: "bold", fontSize: "16px" }}>Task</span>
|
||||
<span style={{ fontWeight: "bold" }}>Task</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onClose}
|
||||
|
||||
Reference in New Issue
Block a user