Refactor potentially relevant details; fix terminal output processing

This commit is contained in:
Saoud Rizwan
2024-09-09 11:23:14 -04:00
parent f35d7bc91a
commit 891ffe6f83
3 changed files with 78 additions and 73 deletions

View File

@@ -257,7 +257,6 @@ export class ClaudeDev {
private askResponseImages?: string[]
private lastMessageTs?: number
private consecutiveMistakeCount: number = 0
private shouldSkipNextApiReqStartedMessage = false
private providerRef: WeakRef<ClaudeDevProvider>
private abort: boolean = false
@@ -445,30 +444,14 @@ export class ClaudeDev {
await this.say("text", task, images)
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a loading spinner as this happens
const taskText = `<task>\n${task}\n</task>`
let imageBlocks: Anthropic.ImageBlockParam[] = this.formatImagesIntoBlocks(images)
await this.say(
"api_req_started",
JSON.stringify({
request: `${taskText}\n\n<potentially_relevant_details>\nLoading...\n</potentially_relevant_details>`,
})
)
this.shouldSkipNextApiReqStartedMessage = true
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${initialDetails}`, // cannot be sent with system prompt since it's cached and these details can change
},
...imageBlocks,
])
})
await this.initiateTaskLoop([
{
type: "text",
text: `<task>\n${task}\n</task>`,
},
...imageBlocks,
])
}
private async resumeTaskFromHistory() {
@@ -647,10 +630,10 @@ export class ClaudeDev {
const combinedText =
`Task resumption: This autonomous coding task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now ${cwd}. If the task has not been completed, retry the last step before interruption and proceed with completing the task.` +
(modifiedOldUserContentText
? `\n\nLast recorded user input before interruption:\n<previous_message>\n${modifiedOldUserContentText}\n</previous_message>\n`
? `\n\nLast recorded user input before interruption:\n<previous_message>\n${modifiedOldUserContentText}\n</previous_message>`
: "") +
(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>`
: "")
const newUserContentImages = newUserContent.filter((block) => block.type === "image")
@@ -664,9 +647,10 @@ export class ClaudeDev {
private async initiateTaskLoop(userContent: UserContent): Promise<void> {
let nextUserContent = userContent
let includeFileDetails = true
while (!this.abort) {
const { didEndLoop } = await this.recursivelyMakeClaudeRequests(nextUserContent)
const { didEndLoop } = await this.recursivelyMakeClaudeRequests(nextUserContent, includeFileDetails)
includeFileDetails = false // we only need file details the first time
// The way this agentic loop works is that claude will be given a task that he then calls tools to complete. unless there's an attempt_completion call, we keep responding back to him with his tool's responses until he either attempt_completion or does not use anymore tools. If he does not use anymore tools, we ask him to consider if he's completed the task and then call attempt_completion, otherwise proceed with completing the task.
// There is a MAX_REQUESTS_PER_TASK limit to prevent infinite requests, but Claude is prompted to finish the task as efficiently as he can.
@@ -1371,7 +1355,6 @@ export class ClaudeDev {
let result = ""
process.on("line", (line) => {
console.log("New line from process:", line)
result += line
sendCommandOutput(line)
})
@@ -1526,7 +1509,10 @@ ${this.customInstructions.trim()}
}
}
async recursivelyMakeClaudeRequests(userContent: UserContent): Promise<ClaudeRequestResult> {
async recursivelyMakeClaudeRequests(
userContent: UserContent,
includeFileDetails: boolean = false
): Promise<ClaudeRequestResult> {
if (this.abort) {
throw new Error("ClaudeDev instance aborted")
}
@@ -1552,19 +1538,33 @@ ${this.customInstructions.trim()}
this.consecutiveMistakeCount = 0
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
await this.say(
"api_req_started",
JSON.stringify({
request:
userContent.map(formatContentBlockToMarkdown).join("\n\n") +
"\n\n<potentially_relevant_details>\nLoading...\n</potentially_relevant_details>",
})
)
// potentially expensive operation
const potentiallyRelevantDetails = await this.getPotentiallyRelevantDetails(includeFileDetails)
// add potentially relevant details as its own text block, separate from tool results
userContent.push({ type: "text", text: await this.getPotentiallyRelevantDetails() })
userContent.push({ type: "text", text: potentiallyRelevantDetails })
await this.addToApiConversationHistory({ role: "user", content: userContent })
if (!this.shouldSkipNextApiReqStartedMessage) {
await this.say(
"api_req_started",
JSON.stringify({ request: userContent.map(formatContentBlockToMarkdown).join("\n\n") })
)
} else {
this.shouldSkipNextApiReqStartedMessage = false
}
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.claudeMessages, (m) => m.say === "api_req_started")
this.claudeMessages[lastApiReqIndex].text = JSON.stringify({
request: userContent.map(formatContentBlockToMarkdown).join("\n\n"),
})
await this.saveClaudeMessages()
await this.providerRef.deref()?.postStateToWebview()
try {
const response = await this.attemptApiRequest()
@@ -1718,23 +1718,7 @@ ${this.customInstructions.trim()}
}
}
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}') Files${
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() {
async getPotentiallyRelevantDetails(includeFileDetails: boolean = false) {
let details = `<potentially_relevant_details>
# VSCode Visible Files
${
@@ -1791,11 +1775,22 @@ ${
if (newOutput) {
details += `\n...\n${newOutput}`
} else {
details += `\n(Still running, no new output)`
// details += `\n(Still running, no new output)` // don't want to show this right after running the command
}
}
}
if (includeFileDetails) {
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
const files = await listFiles(cwd, !isDesktop)
const result = this.formatFilesList(cwd, files)
details += `\n\n# Current Working Directory ('${cwd}') Files${
isDesktop
? "\n(Desktop so only top-level contents shown for brevity, use list_files to explore further if necessary)"
: ""
}\n${result}`
}
details += "\n</potentially_relevant_details>"
return details
}

View File

@@ -277,13 +277,19 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
let isFirstChunk = true
let didOutputNonCommand = false
for await (let data of stream) {
console.log("original chunk:", data)
if (isFirstChunk) {
/*
The first chunk we get from this stream needs to be processed to be more human readable, ie remove vscode's custom escape sequences and identifiers, removing duplicate first char bug, etc.
*/
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const vscodeSequenceRegex = /\x1b\]633;.[^\x07]*\x07/g
data = stripAnsi(data.replace(vscodeSequenceRegex, ""))
const lastMatch = [...data.matchAll(vscodeSequenceRegex)].pop()
if (lastMatch && lastMatch.index !== undefined) {
data = data.slice(lastMatch.index + lastMatch[0].length)
}
// remove ansi
data = stripAnsi(data)
// Split data by newlines
let lines = data ? data.split("\n") : []
// Remove non-human readable characters from the first line
@@ -320,7 +326,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
data = lines.join("\n")
}
console.log(`Received data chunk for terminal:`, data)
console.log(`parsed chunk:`, data)
this.fullOutput += data
if (this.isListening) {
console.log(`Emitting data for terminal`)

View File

@@ -444,25 +444,29 @@ const ChatRowContent = ({ message, isExpanded, onToggleExpand, lastModifiedMessa
<div
style={{
display: "flex",
alignItems: "center",
flexDirection: "column",
backgroundColor: "rgba(255, 191, 0, 0.1)",
padding: 8,
borderRadius: 3,
fontSize: 12,
}}>
<i
className="codicon codicon-warning"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
}}></i>
<span>
Shell integration is not available! Claude will not be able to see the output of the
command. Please update to the latest version of VSCode (
{"CMD/CTRL + Shift + P → Update"}) and ensure you are using one of the following
shells: bash, zsh, fish, or PowerShell.
</span>
<div style={{ display: "flex", alignItems: "center", marginBottom: 4 }}>
<i
className="codicon codicon-warning"
style={{
marginRight: 8,
fontSize: 18,
color: "#FFA500",
}}></i>
<span style={{ fontWeight: 500, color: "#FFA500" }}>
Shell Integration Unavailable
</span>
</div>
<div>
Claude won't be able to view the command's output. Please update VSCode (CMD/CTRL +
Shift + P Update) and make sure you're using a supported shell: bash, zsh, fish,
or PowerShell.
</div>
</div>
</>
)