mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -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 { HistoryItem } from "./shared/HistoryItem"
|
||||||
import { Tool, ToolName } from "./shared/Tool"
|
import { Tool, ToolName } from "./shared/Tool"
|
||||||
import { ClaudeAskResponse } from "./shared/WebviewMessage"
|
import { ClaudeAskResponse } from "./shared/WebviewMessage"
|
||||||
import { findLast, findLastIndex } from "./utils"
|
import { findLast, findLastIndex, formatContentBlockToMarkdown } from "./utils"
|
||||||
import { truncateHalfConversation } from "./utils/context-management"
|
import { truncateHalfConversation } from "./utils/context-management"
|
||||||
import { regexSearchFiles } from "./utils/ripgrep"
|
import { regexSearchFiles } from "./utils/ripgrep"
|
||||||
import { extractTextFromFile } from "./utils/extract-text"
|
import { extractTextFromFile } from "./utils/extract-text"
|
||||||
@@ -281,7 +281,7 @@ export class ClaudeDev {
|
|||||||
) {
|
) {
|
||||||
this.providerRef = new WeakRef(provider)
|
this.providerRef = new WeakRef(provider)
|
||||||
this.api = buildApiHandler(apiConfiguration)
|
this.api = buildApiHandler(apiConfiguration)
|
||||||
this.terminalManager = new TerminalManager(provider.context)
|
this.terminalManager = new TerminalManager()
|
||||||
this.customInstructions = customInstructions
|
this.customInstructions = customInstructions
|
||||||
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
|
||||||
|
|
||||||
@@ -445,31 +445,6 @@ export class ClaudeDev {
|
|||||||
await this.providerRef.deref()?.postStateToWebview()
|
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> {
|
private async startTask(task?: string, images?: string[]): Promise<void> {
|
||||||
// conversationHistory (for API) and claudeMessages (for webview) need to be in sync
|
// 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)
|
// 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(
|
await this.say(
|
||||||
"api_req_started",
|
"api_req_started",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
request: this.api.createUserReadableRequest([
|
request: `${taskText}\n\n<potentially_relevant_details>\nLoading...\n</potentially_relevant_details>`,
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: `${taskText}\n\n<potentially_relevant_details>(see getPotentiallyRelevantDetails in src/ClaudeDev.ts)</potentially_relevant_details>`,
|
|
||||||
},
|
|
||||||
...imageBlocks,
|
|
||||||
]),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.shouldSkipNextApiReqStartedMessage = true
|
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([
|
await this.initiateTaskLoop([
|
||||||
{
|
{
|
||||||
type: "text",
|
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,
|
...imageBlocks,
|
||||||
])
|
])
|
||||||
@@ -687,8 +660,7 @@ export class ClaudeDev {
|
|||||||
: "") +
|
: "") +
|
||||||
(newUserContentText
|
(newUserContentText
|
||||||
? `\n\nNew instructions for task continuation:\n<user_message>\n${newUserContentText}\n</user_message>\n`
|
? `\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 newUserContentImages = newUserContent.filter((block) => block.type === "image")
|
||||||
const combinedModifiedOldUserContentWithNewUserContent: UserContent = (
|
const combinedModifiedOldUserContentWithNewUserContent: UserContent = (
|
||||||
@@ -781,21 +753,12 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
async writeToFile(relPath?: string, newContent?: string): Promise<ToolResponse> {
|
async writeToFile(relPath?: string, newContent?: string): Promise<ToolResponse> {
|
||||||
if (relPath === undefined) {
|
if (relPath === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use write_to_file without value for required parameter 'path'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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) {
|
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++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
try {
|
try {
|
||||||
@@ -991,9 +954,9 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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()
|
const editedContent = updatedDocument.getText()
|
||||||
@@ -1070,9 +1033,11 @@ export class ClaudeDev {
|
|||||||
diff: this.createPrettyPatch(relPath, normalizedNewContent, normalizedEditedContent),
|
diff: this.createPrettyPatch(relPath, normalizedNewContent, normalizedEditedContent),
|
||||||
} as ClaudeSayTool)
|
} 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 {
|
} else {
|
||||||
return `The content was successfully saved to ${relPath}.`
|
return this.formatToolResult(`The content was successfully saved to ${relPath}.`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
||||||
@@ -1080,7 +1045,7 @@ export class ClaudeDev {
|
|||||||
"error",
|
"error",
|
||||||
`Error writing file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
`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> {
|
async readFile(relPath?: string): Promise<ToolResponse> {
|
||||||
if (relPath === undefined) {
|
if (relPath === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use read_file without value for required parameter 'path'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
try {
|
try {
|
||||||
@@ -1176,9 +1137,9 @@ export class ClaudeDev {
|
|||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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",
|
||||||
`Error reading file:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
`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> {
|
async listFiles(relDirPath?: string, recursiveRaw?: string): Promise<ToolResponse> {
|
||||||
if (relDirPath === undefined) {
|
if (relDirPath === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use list_files without value for required parameter 'path'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
try {
|
try {
|
||||||
@@ -1221,13 +1178,13 @@ export class ClaudeDev {
|
|||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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) {
|
} catch (error) {
|
||||||
const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}`
|
const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}`
|
||||||
await this.say(
|
await this.say(
|
||||||
@@ -1236,7 +1193,7 @@ export class ClaudeDev {
|
|||||||
error.message ?? JSON.stringify(serializeError(error), null, 2)
|
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> {
|
async listCodeDefinitionNames(relDirPath?: string): Promise<ToolResponse> {
|
||||||
if (relDirPath === undefined) {
|
if (relDirPath === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use list_code_definition_names without value for required parameter 'path'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
try {
|
try {
|
||||||
@@ -1325,13 +1278,13 @@ export class ClaudeDev {
|
|||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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) {
|
} catch (error) {
|
||||||
const errorString = `Error parsing source code definitions: ${JSON.stringify(serializeError(error))}`
|
const errorString = `Error parsing source code definitions: ${JSON.stringify(serializeError(error))}`
|
||||||
await this.say(
|
await this.say(
|
||||||
@@ -1340,26 +1293,18 @@ export class ClaudeDev {
|
|||||||
error.message ?? JSON.stringify(serializeError(error), null, 2)
|
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> {
|
async searchFiles(relDirPath: string, regex: string, filePattern?: string): Promise<ToolResponse> {
|
||||||
if (relDirPath === undefined) {
|
if (relDirPath === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use search_files without value for required parameter 'path'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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) {
|
if (regex === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
`Claude tried to use search_files without value for required parameter 'regex'. Retrying...`
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
try {
|
try {
|
||||||
@@ -1381,40 +1326,36 @@ export class ClaudeDev {
|
|||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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) {
|
} catch (error) {
|
||||||
const errorString = `Error searching files: ${JSON.stringify(serializeError(error))}`
|
const errorString = `Error searching files: ${JSON.stringify(serializeError(error))}`
|
||||||
await this.say(
|
await this.say(
|
||||||
"error",
|
"error",
|
||||||
`Error searching files:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`
|
`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> {
|
async executeCommand(command?: string, returnEmptyStringOnSuccess: boolean = false): Promise<ToolResponse> {
|
||||||
if (command === undefined) {
|
if (command === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use execute_command without value for required parameter 'command'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
const { response, text, images } = await this.ask("command", command)
|
const { response, text, images } = await this.ask("command", command)
|
||||||
if (response !== "yesButtonTapped") {
|
if (response !== "yesButtonTapped") {
|
||||||
if (response === "messageResponse") {
|
if (response === "messageResponse") {
|
||||||
await this.say("user_feedback", text, images)
|
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 {
|
try {
|
||||||
@@ -1439,7 +1380,7 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
let result = ""
|
let result = ""
|
||||||
process.on("line", (line) => {
|
process.on("line", (line) => {
|
||||||
console.log("sending line from here", line)
|
console.log("New line from process:", line)
|
||||||
result += line
|
result += line
|
||||||
sendCommandOutput(line)
|
sendCommandOutput(line)
|
||||||
})
|
})
|
||||||
@@ -1455,10 +1396,8 @@ export class ClaudeDev {
|
|||||||
|
|
||||||
if (userFeedback) {
|
if (userFeedback) {
|
||||||
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
await this.say("user_feedback", userFeedback.text, userFeedback.images)
|
||||||
return this.formatIntoToolResponse(
|
return this.formatToolResponseWithImages(
|
||||||
`Command Output:\n${result}\n\nThe user interrupted the command and provided the following feedback:\n<feedback>\n${
|
`Command Output:\n${result}\n\nThe user interrupted the command and provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
|
||||||
userFeedback.text
|
|
||||||
}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`,
|
|
||||||
userFeedback.images
|
userFeedback.images
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1467,39 +1406,31 @@ export class ClaudeDev {
|
|||||||
if (returnEmptyStringOnSuccess) {
|
if (returnEmptyStringOnSuccess) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`
|
return await this.formatToolResult(`Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
|
let errorMessage = error.message || JSON.stringify(serializeError(error), null, 2)
|
||||||
const errorString = `Error executing command:\n${errorMessage}`
|
const errorString = `Error executing command:\n${errorMessage}`
|
||||||
await this.say("error", `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> {
|
async askFollowupQuestion(question?: string): Promise<ToolResponse> {
|
||||||
if (question === undefined) {
|
if (question === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use ask_followup_question without value for required parameter 'question'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
const { text, images } = await this.ask("followup", question)
|
const { text, images } = await this.ask("followup", question)
|
||||||
await this.say("user_feedback", text ?? "", images)
|
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> {
|
async attemptCompletion(result?: string, command?: string): Promise<ToolResponse> {
|
||||||
// result is required, command is optional
|
// result is required, command is optional
|
||||||
if (result === undefined) {
|
if (result === undefined) {
|
||||||
await this.say(
|
|
||||||
"error",
|
|
||||||
"Claude tried to use attempt_completion without value for required parameter 'result'. Retrying..."
|
|
||||||
)
|
|
||||||
this.consecutiveMistakeCount++
|
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
|
this.consecutiveMistakeCount = 0
|
||||||
let resultToSend = result
|
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)
|
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)
|
await this.say("user_feedback", text ?? "", images)
|
||||||
return this.formatIntoToolResponse(
|
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>\n\n${await this.getPotentiallyRelevantDetails()}`,
|
`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
|
images
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1600,7 +1531,7 @@ ${this.customInstructions.trim()}
|
|||||||
...[
|
...[
|
||||||
{
|
{
|
||||||
type: "text",
|
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,
|
} as Anthropic.Messages.TextBlockParam,
|
||||||
...this.formatImagesIntoBlocks(images),
|
...this.formatImagesIntoBlocks(images),
|
||||||
]
|
]
|
||||||
@@ -1609,15 +1540,15 @@ ${this.customInstructions.trim()}
|
|||||||
this.consecutiveMistakeCount = 0
|
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 })
|
await this.addToApiConversationHistory({ role: "user", content: userContent })
|
||||||
|
|
||||||
if (!this.shouldSkipNextApiReqStartedMessage) {
|
if (!this.shouldSkipNextApiReqStartedMessage) {
|
||||||
await this.say(
|
await this.say(
|
||||||
"api_req_started",
|
"api_req_started",
|
||||||
// what the user sees in the webview
|
JSON.stringify({ request: userContent.map(formatContentBlockToMarkdown).join("\n\n") })
|
||||||
JSON.stringify({
|
|
||||||
request: this.api.createUserReadableRequest(userContent),
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
this.shouldSkipNextApiReqStartedMessage = false
|
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>
|
let details = `<potentially_relevant_details>
|
||||||
# VSCode Visible Files:
|
# VSCode Visible Files:
|
||||||
${
|
${
|
||||||
@@ -1769,25 +1741,76 @@ ${
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((absolutePath) => path.relative(cwd, absolutePath))
|
.map((absolutePath) => path.relative(cwd, absolutePath))
|
||||||
.join("\n") || "(No tabs open)"
|
.join("\n") || "(No tabs open)"
|
||||||
}
|
}`
|
||||||
`
|
|
||||||
|
|
||||||
if (verbose) {
|
const busyTerminals = this.terminalManager.getBusyTerminals()
|
||||||
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
if (busyTerminals.length > 0) {
|
||||||
const files = await listFiles(cwd, !isDesktop)
|
details += "\n\n# Active Terminals:"
|
||||||
const result = this.formatFilesList(cwd, files)
|
for (const busyTerminal of busyTerminals) {
|
||||||
details += `\n# Current Working Directory ('${cwd}') File Structure:${
|
details += `\n## Original command:\n${busyTerminal.lastCommand}`
|
||||||
isDesktop
|
const newOutput = this.terminalManager.getUnretrievedOutput(busyTerminal.id)
|
||||||
? "\n(Desktop so only top-level contents shown for brevity, use list_files to explore further if necessary)"
|
if (newOutput) {
|
||||||
: ""
|
details += `\n## New output since last check:\n${newOutput}`
|
||||||
}:\n${result}\n`
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
async formatGenericToolFeedback(feedback?: string) {
|
async formatToolDeniedFeedback(feedback?: string) {
|
||||||
return `The user denied this operation and provided the following feedback:\n<feedback>\n${feedback}\n</feedback>\n\n${await this.getPotentiallyRelevantDetails()}`
|
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 { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||||
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../shared/api"
|
import { anthropicDefaultModelId, AnthropicModelId, anthropicModels, ApiHandlerOptions, ModelInfo } from "../shared/api"
|
||||||
|
|
||||||
export class AnthropicHandler implements ApiHandler {
|
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 } {
|
getModel(): { id: AnthropicModelId; info: ModelInfo } {
|
||||||
const modelId = this.options.apiModelId
|
const modelId = this.options.apiModelId
|
||||||
if (modelId && modelId in anthropicModels) {
|
if (modelId && modelId in anthropicModels) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"
|
||||||
import { Anthropic } from "@anthropic-ai/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"
|
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../shared/api"
|
||||||
|
|
||||||
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
|
||||||
@@ -39,24 +39,6 @@ export class AwsBedrockHandler implements ApiHandler {
|
|||||||
return { message }
|
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 } {
|
getModel(): { id: BedrockModelId; info: ModelInfo } {
|
||||||
const modelId = this.options.apiModelId
|
const modelId = this.options.apiModelId
|
||||||
if (modelId && modelId in bedrockModels) {
|
if (modelId && modelId in bedrockModels) {
|
||||||
|
|||||||
@@ -19,15 +19,6 @@ export interface ApiHandler {
|
|||||||
tools: Anthropic.Messages.Tool[]
|
tools: Anthropic.Messages.Tool[]
|
||||||
): Promise<ApiHandlerMessageResponse>
|
): Promise<ApiHandlerMessageResponse>
|
||||||
|
|
||||||
createUserReadableRequest(
|
|
||||||
userContent: Array<
|
|
||||||
| Anthropic.TextBlockParam
|
|
||||||
| Anthropic.ImageBlockParam
|
|
||||||
| Anthropic.ToolUseBlockParam
|
|
||||||
| Anthropic.ToolResultBlockParam
|
|
||||||
>
|
|
||||||
): any
|
|
||||||
|
|
||||||
getModel(): { id: string; info: ModelInfo }
|
getModel(): { id: string; info: ModelInfo }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,31 +41,3 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
|||||||
return new AnthropicHandler(options)
|
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 { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import OpenAI from "openai"
|
import OpenAI from "openai"
|
||||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
||||||
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
||||||
|
|
||||||
@@ -48,23 +48,6 @@ export class OllamaHandler implements ApiHandler {
|
|||||||
return { message: anthropicMessage }
|
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 } {
|
getModel(): { id: string; info: ModelInfo } {
|
||||||
return {
|
return {
|
||||||
id: this.options.ollamaModelId ?? "",
|
id: this.options.ollamaModelId ?? "",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import OpenAI from "openai"
|
import OpenAI from "openai"
|
||||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../shared/api"
|
||||||
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
import { convertToAnthropicMessage, convertToOpenAiMessages } from "../utils/openai-format"
|
||||||
|
|
||||||
@@ -48,23 +48,6 @@ export class OpenAiHandler implements ApiHandler {
|
|||||||
return { message: anthropicMessage }
|
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 } {
|
getModel(): { id: string; info: ModelInfo } {
|
||||||
return {
|
return {
|
||||||
id: this.options.openAiModelId ?? "",
|
id: this.options.openAiModelId ?? "",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
import OpenAI from "openai"
|
import OpenAI from "openai"
|
||||||
import { ApiHandler, ApiHandlerMessageResponse, withoutImageData } from "."
|
import { ApiHandler, ApiHandlerMessageResponse } from "."
|
||||||
import {
|
import {
|
||||||
ApiHandlerOptions,
|
ApiHandlerOptions,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
@@ -177,24 +177,6 @@ export class OpenRouterHandler implements ApiHandler {
|
|||||||
return completion
|
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 } {
|
getModel(): { id: OpenRouterModelId; info: ModelInfo } {
|
||||||
const modelId = this.options.apiModelId
|
const modelId = this.options.apiModelId
|
||||||
if (modelId && modelId in openRouterModels) {
|
if (modelId && modelId in openRouterModels) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"
|
||||||
import { Anthropic } from "@anthropic-ai/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"
|
import { ApiHandlerOptions, ModelInfo, vertexDefaultModelId, VertexModelId, vertexModels } from "../shared/api"
|
||||||
|
|
||||||
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
// https://docs.anthropic.com/en/api/claude-on-vertex-ai
|
||||||
@@ -33,24 +33,6 @@ export class VertexHandler implements ApiHandler {
|
|||||||
return { message }
|
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 } {
|
getModel(): { id: VertexModelId; info: ModelInfo } {
|
||||||
const modelId = this.options.apiModelId
|
const modelId = this.options.apiModelId
|
||||||
if (modelId && modelId in vertexModels) {
|
if (modelId && modelId in vertexModels) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
import delay from "delay"
|
import pWaitFor from "p-wait-for"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TerminalManager:
|
TerminalManager:
|
||||||
@@ -21,6 +21,14 @@ Enables flexible command execution:
|
|||||||
- Continue execution in background
|
- Continue execution in background
|
||||||
- Retrieve missed output later
|
- 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:
|
Example:
|
||||||
|
|
||||||
const terminalManager = new TerminalManager(context);
|
const terminalManager = new TerminalManager(context);
|
||||||
@@ -41,77 +49,75 @@ process.continue();
|
|||||||
// Later, if you need to get the unretrieved output:
|
// Later, if you need to get the unretrieved output:
|
||||||
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
|
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
|
||||||
console.log('Unretrieved output:', unretrievedOutput);
|
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 {
|
export class TerminalManager {
|
||||||
private terminals: TerminalInfo[] = []
|
private terminals: TerminalInfo[] = []
|
||||||
private processes: Map<number, TerminalProcess> = new Map()
|
private processes: Map<number, TerminalProcess> = new Map()
|
||||||
private context: vscode.ExtensionContext
|
|
||||||
private nextTerminalId = 1
|
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 {
|
runCommand(terminalInfo: TerminalInfo, command: string, cwd: string): TerminalProcessResultPromise {
|
||||||
terminalInfo.busy = true
|
terminalInfo.busy = true
|
||||||
terminalInfo.lastCommand = command
|
terminalInfo.lastCommand = command
|
||||||
|
const process = new TerminalProcess()
|
||||||
const process = new TerminalProcess(terminalInfo, command)
|
|
||||||
|
|
||||||
this.processes.set(terminalInfo.id, process)
|
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) => {
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
process.once(CONTINUE_EVENT, () => {
|
process.once("continue", () => {
|
||||||
console.log("2")
|
console.log(`continue received for terminal ${terminalInfo.id}`)
|
||||||
resolve()
|
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 shell integration is already active, run the command immediately
|
||||||
if (terminalInfo.terminal.shellIntegration) {
|
if (terminalInfo.terminal.shellIntegration) {
|
||||||
|
console.log(`Shell integration active for terminal ${terminalInfo.id}, running command immediately`)
|
||||||
process.waitForShellIntegration = false
|
process.waitForShellIntegration = false
|
||||||
process.run()
|
process.run(terminalInfo.terminal, command)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
// User doesn't have shell integration API available, run command the old way
|
console.log(`Waiting for shell integration for terminal ${terminalInfo.id}`)
|
||||||
process.waitForShellIntegration = false
|
// docs recommend waiting 3s for shell integration to activate
|
||||||
process.run()
|
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)
|
return mergePromise(process, promise)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
||||||
const availableTerminal = this.terminals.find((t) => {
|
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
|
return false
|
||||||
}
|
}
|
||||||
const terminalCwd = t.terminal.shellIntegration?.cwd // one of claude's commands could have changed the cwd of the terminal
|
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
|
return vscode.Uri.file(cwd).fsPath === terminalCwd.fsPath
|
||||||
})
|
})
|
||||||
if (availableTerminal) {
|
if (availableTerminal) {
|
||||||
console.log("reusing terminal", availableTerminal.id)
|
console.log("Reusing terminal", availableTerminal.id)
|
||||||
return availableTerminal
|
return availableTerminal
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,63 +146,10 @@ export class TerminalManager {
|
|||||||
return newTerminalInfo
|
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 }[] {
|
getBusyTerminals(): { id: number; lastCommand: string }[] {
|
||||||
return this.terminals.filter((t) => t.busy).map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
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 {
|
getUnretrievedOutput(terminalId: number): string {
|
||||||
const process = this.processes.get(terminalId)
|
const process = this.processes.get(terminalId)
|
||||||
if (!process) {
|
if (!process) {
|
||||||
@@ -206,19 +159,14 @@ export class TerminalManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disposeAll() {
|
disposeAll() {
|
||||||
for (const info of this.terminals) {
|
// for (const info of this.terminals) {
|
||||||
info.terminal.dispose() // todo do we want to do this? test with tab view closing it
|
// //info.terminal.dispose() // dont want to dispose terminals when task is aborted
|
||||||
}
|
// }
|
||||||
this.terminals = []
|
this.terminals = []
|
||||||
this.processes.clear()
|
this.processes.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasShellIntegrationApis(): boolean {
|
|
||||||
const [major, minor] = vscode.version.split(".").map(Number)
|
|
||||||
return major > 1 || (major === 1 && minor >= 93)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TerminalInfo {
|
interface TerminalInfo {
|
||||||
terminal: vscode.Terminal
|
terminal: vscode.Terminal
|
||||||
busy: boolean
|
busy: boolean
|
||||||
@@ -226,54 +174,59 @@ interface TerminalInfo {
|
|||||||
id: number
|
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
|
waitForShellIntegration: boolean = true
|
||||||
private isListening: boolean = true
|
private isListening: boolean = true
|
||||||
private buffer: string = ""
|
private buffer: string = ""
|
||||||
private execution?: vscode.TerminalShellExecution
|
|
||||||
private stream?: AsyncIterable<string>
|
|
||||||
private fullOutput: string = ""
|
private fullOutput: string = ""
|
||||||
private lastRetrievedIndex: number = 0
|
private lastRetrievedIndex: number = 0
|
||||||
|
|
||||||
constructor(public terminalInfo: TerminalInfo, private command: string) {
|
// constructor() {
|
||||||
super()
|
// super()
|
||||||
}
|
|
||||||
|
|
||||||
async run() {
|
async run(terminal: vscode.Terminal, command: string) {
|
||||||
if (this.terminalInfo.terminal.shellIntegration) {
|
if (terminal.shellIntegration) {
|
||||||
this.execution = this.terminalInfo.terminal.shellIntegration.executeCommand(this.command)
|
console.log(`Shell integration available for terminal`)
|
||||||
this.stream = this.execution.read()
|
const execution = terminal.shellIntegration.executeCommand(command)
|
||||||
|
const stream = execution.read()
|
||||||
// todo: need to handle errors
|
// todo: need to handle errors
|
||||||
let isFirstChunk = true // ignore first chunk since it's vscode shell integration marker
|
for await (const data of stream) {
|
||||||
for await (const data of this.stream) {
|
console.log(`Received data chunk for terminal:`, data)
|
||||||
console.log("data", data)
|
this.fullOutput += data
|
||||||
if (!isFirstChunk) {
|
if (this.isListening) {
|
||||||
this.fullOutput += data
|
console.log(`Emitting data for terminal`)
|
||||||
if (this.isListening) {
|
this.emitIfEol(data)
|
||||||
this.emitIfEol(data)
|
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
||||||
this.lastRetrievedIndex = this.fullOutput.length - this.buffer.length
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isFirstChunk = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit any remaining content in the buffer
|
// Emit any remaining content in the buffer
|
||||||
if (this.buffer && this.isListening) {
|
if (this.buffer && this.isListening) {
|
||||||
|
console.log(`Emitting remaining buffer for terminal:`, this.buffer.trim())
|
||||||
this.emit("line", this.buffer.trim())
|
this.emit("line", this.buffer.trim())
|
||||||
this.buffer = ""
|
this.buffer = ""
|
||||||
this.lastRetrievedIndex = this.fullOutput.length
|
this.lastRetrievedIndex = this.fullOutput.length
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit(CONTINUE_EVENT)
|
console.log(`Command execution completed for terminal`)
|
||||||
|
this.emit("continue")
|
||||||
|
this.emit("completed")
|
||||||
} else {
|
} 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
|
// For terminals without shell integration, we can't know when the command completes
|
||||||
// So we'll just emit the continue event after a delay
|
// So we'll just emit the continue event after a delay
|
||||||
setTimeout(() => {
|
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
|
}, 2000) // Adjust this delay as needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,13 +247,17 @@ export class TerminalProcess extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
continue() {
|
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.isListening = false
|
||||||
this.removeAllListeners("line")
|
this.removeAllListeners("line")
|
||||||
this.emit(CONTINUE_EVENT)
|
this.emit("continue")
|
||||||
}
|
|
||||||
|
|
||||||
isStillListening() {
|
|
||||||
return this.isListening
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnretrievedOutput(): string {
|
getUnretrievedOutput(): string {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os from "os"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
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.
|
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
||||||
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
|
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:
|
block:
|
||||||
| Anthropic.TextBlockParam
|
| Anthropic.TextBlockParam
|
||||||
| Anthropic.ImageBlockParam
|
| Anthropic.ImageBlockParam
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ReactMarkdown from "react-markdown"
|
|||||||
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
import { ClaudeMessage, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||||
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
||||||
import CodeAccordian from "./CodeAccordian"
|
import CodeAccordian from "./CodeAccordian"
|
||||||
import CodeBlock from "./CodeBlock"
|
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
@@ -352,8 +352,8 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div style={{ marginTop: "10px" }}>
|
<div style={{ marginTop: "10px" }}>
|
||||||
<CodeAccordian
|
<CodeAccordian
|
||||||
code={JSON.stringify(JSON.parse(message.text || "{}").request, null, 2)}
|
code={JSON.parse(message.text || "{}").request}
|
||||||
language="json"
|
language="markdown"
|
||||||
isExpanded={true}
|
isExpanded={true}
|
||||||
onToggleExpand={onToggleExpand}
|
onToggleExpand={onToggleExpand}
|
||||||
/>
|
/>
|
||||||
@@ -516,19 +516,32 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa
|
|||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
border: "1px solid var(--vscode-sideBar-border)",
|
border: "1px solid var(--vscode-sideBar-border)",
|
||||||
overflow: "hidden",
|
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>
|
</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":
|
case "completion_result":
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import styled from "styled-components"
|
|||||||
import { visit } from "unist-util-visit"
|
import { visit } from "unist-util-visit"
|
||||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
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.
|
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",
|
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 {
|
pre {
|
||||||
background-color: ${BG_COLOR};
|
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: max-content;
|
min-width: ${({ forceWrap }) => (forceWrap ? "auto" : "max-content")};
|
||||||
padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +58,7 @@ const StyledMarkdown = styled.div`
|
|||||||
}
|
}
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
border-radius: 5px;
|
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-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||||
font-family: var(--vscode-editor-font-family);
|
font-family: var(--vscode-editor-font-family);
|
||||||
}
|
}
|
||||||
@@ -53,7 +68,7 @@ const StyledMarkdown = styled.div`
|
|||||||
color: #f78383;
|
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,
|
font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||||
@@ -84,7 +99,7 @@ const StyledPre = styled.pre<{ theme: any }>`
|
|||||||
.join("")}
|
.join("")}
|
||||||
`
|
`
|
||||||
|
|
||||||
const CodeBlock = memo(({ source }: { source?: string }) => {
|
const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
|
||||||
const { theme } = useExtensionState()
|
const { theme } = useExtensionState()
|
||||||
const [reactContent, setMarkdownSource] = useRemark({
|
const [reactContent, setMarkdownSource] = useRemark({
|
||||||
remarkPlugins: [
|
remarkPlugins: [
|
||||||
@@ -121,11 +136,11 @@ const CodeBlock = memo(({ source }: { source?: string }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
overflowY: "auto",
|
overflowY: forceWrap ? "visible" : "auto",
|
||||||
maxHeight: "100%",
|
maxHeight: forceWrap ? "none" : "100%",
|
||||||
backgroundColor: BG_COLOR,
|
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||||
}}>
|
}}>
|
||||||
<StyledMarkdown>{reactContent}</StyledMarkdown>
|
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}>
|
}}>
|
||||||
<span style={{ fontWeight: "bold", fontSize: "16px" }}>Task</span>
|
<span style={{ fontWeight: "bold" }}>Task</span>
|
||||||
<VSCodeButton
|
<VSCodeButton
|
||||||
appearance="icon"
|
appearance="icon"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
|||||||
Reference in New Issue
Block a user