mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Add ChatRow and handle different message types
This commit is contained in:
108
src/ClaudeDev.ts
108
src/ClaudeDev.ts
@@ -11,7 +11,7 @@ import { DEFAULT_MAX_REQUESTS_PER_TASK } from "./shared/Constants"
|
||||
import { Tool, ToolName } from "./shared/Tool"
|
||||
import { ClaudeAsk, ClaudeSay, ExtensionMessage } from "./shared/ExtensionMessage"
|
||||
import * as vscode from "vscode"
|
||||
import pWaitFor from 'p-wait-for'
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { ClaudeAskResponse } from "./shared/WebviewMessage"
|
||||
import { SidebarProvider } from "./providers/SidebarProvider"
|
||||
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
|
||||
@@ -159,54 +159,54 @@ export class ClaudeDev {
|
||||
private conversationHistory: Anthropic.MessageParam[] = []
|
||||
private maxRequestsPerTask: number
|
||||
private requestCount = 0
|
||||
private askResponse?: ClaudeAskResponse
|
||||
private askResponseText?: string
|
||||
private providerRef: WeakRef<SidebarProvider>
|
||||
private askResponse?: ClaudeAskResponse
|
||||
private askResponseText?: string
|
||||
private providerRef: WeakRef<SidebarProvider>
|
||||
|
||||
constructor(provider: SidebarProvider, task: string, apiKey: string, maxRequestsPerTask?: number) {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.providerRef = new WeakRef(provider)
|
||||
this.client = new Anthropic({ apiKey })
|
||||
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
||||
|
||||
this.startTask(task)
|
||||
this.startTask(task)
|
||||
}
|
||||
|
||||
updateApiKey(apiKey: string) {
|
||||
this.client = new Anthropic({ apiKey })
|
||||
}
|
||||
|
||||
updateMaxRequestsPerTask(maxRequestsPerTask: number | undefined) {
|
||||
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
||||
}
|
||||
|
||||
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string) {
|
||||
this.askResponse = askResponse
|
||||
this.askResponseText = text
|
||||
}
|
||||
|
||||
async ask(type: ClaudeAsk, question: string): Promise<{response: ClaudeAskResponse, text?: string}> {
|
||||
this.askResponse = undefined
|
||||
this.askResponseText = undefined
|
||||
await this.providerRef.deref()?.addClaudeMessage({ type: "ask", ask: type, text: question })
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
await pWaitFor(() => this.askResponse !== undefined, { interval: 100 })
|
||||
const result = { response: this.askResponse!, text: this.askResponseText }
|
||||
this.askResponse = undefined
|
||||
this.askResponseText = undefined
|
||||
return result
|
||||
updateApiKey(apiKey: string) {
|
||||
this.client = new Anthropic({ apiKey })
|
||||
}
|
||||
|
||||
async say(type: ClaudeSay, question: string): Promise<undefined> {
|
||||
await this.providerRef.deref()?.addClaudeMessage({ type: "say", say: type, text: question })
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
updateMaxRequestsPerTask(maxRequestsPerTask: number | undefined) {
|
||||
this.maxRequestsPerTask = maxRequestsPerTask ?? DEFAULT_MAX_REQUESTS_PER_TASK
|
||||
}
|
||||
|
||||
async handleWebviewAskResponse(askResponse: ClaudeAskResponse, text?: string) {
|
||||
this.askResponse = askResponse
|
||||
this.askResponseText = text
|
||||
}
|
||||
|
||||
async ask(type: ClaudeAsk, question: string): Promise<{ response: ClaudeAskResponse; text?: string }> {
|
||||
this.askResponse = undefined
|
||||
this.askResponseText = undefined
|
||||
await this.providerRef.deref()?.addClaudeMessage({ ts: Date.now(), type: "ask", ask: type, text: question })
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
await pWaitFor(() => this.askResponse !== undefined, { interval: 100 })
|
||||
const result = { response: this.askResponse!, text: this.askResponseText }
|
||||
this.askResponse = undefined
|
||||
this.askResponseText = undefined
|
||||
return result
|
||||
}
|
||||
|
||||
async say(type: ClaudeSay, text: string): Promise<undefined> {
|
||||
await this.providerRef.deref()?.addClaudeMessage({ ts: Date.now(), type: "say", say: type, text: text })
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
}
|
||||
|
||||
private async startTask(task: 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)
|
||||
// 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)
|
||||
await this.providerRef.deref()?.setClaudeMessages([])
|
||||
await this.providerRef.deref()?.postStateToWebview()
|
||||
|
||||
|
||||
// Get all relevant context for the task
|
||||
const filesInCurrentDir = await this.listFiles()
|
||||
|
||||
@@ -237,9 +237,9 @@ ${filesInCurrentDir}`
|
||||
// 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.
|
||||
|
||||
const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
|
||||
//const totalCost = this.calculateApiCost(totalInputTokens, totalOutputTokens)
|
||||
if (didCompleteTask) {
|
||||
this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
|
||||
//this.say("task_completed", `Task completed. Total API usage cost: ${totalCost}`)
|
||||
break
|
||||
} else {
|
||||
this.say(
|
||||
@@ -272,13 +272,13 @@ ${filesInCurrentDir}`
|
||||
}
|
||||
|
||||
// Calculates cost of a Claude 3.5 Sonnet API request
|
||||
calculateApiCost(inputTokens: number, outputTokens: number): string {
|
||||
calculateApiCost(inputTokens: number, outputTokens: number): number {
|
||||
const INPUT_COST_PER_MILLION = 3.0 // $3 per million input tokens
|
||||
const OUTPUT_COST_PER_MILLION = 15.0 // $15 per million output tokens
|
||||
const inputCost = (inputTokens / 1_000_000) * INPUT_COST_PER_MILLION
|
||||
const outputCost = (outputTokens / 1_000_000) * OUTPUT_COST_PER_MILLION
|
||||
const totalCost = inputCost + outputCost
|
||||
return `$${totalCost.toFixed(4)}`
|
||||
return totalCost
|
||||
}
|
||||
|
||||
async writeToFile(filePath: string, newContent: string): Promise<string> {
|
||||
@@ -303,7 +303,7 @@ ${filesInCurrentDir}`
|
||||
}
|
||||
} catch (error) {
|
||||
const errorString = `Error writing file: ${JSON.stringify(serializeError(error))}`
|
||||
this.say("error", errorString)
|
||||
this.say("error", errorString)
|
||||
return errorString
|
||||
}
|
||||
}
|
||||
@@ -312,16 +312,16 @@ ${filesInCurrentDir}`
|
||||
try {
|
||||
return await fs.readFile(filePath, "utf-8")
|
||||
} catch (error) {
|
||||
const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}`
|
||||
this.say("error", errorString)
|
||||
const errorString = `Error reading file: ${JSON.stringify(serializeError(error))}`
|
||||
this.say("error", errorString)
|
||||
return errorString
|
||||
}
|
||||
}
|
||||
|
||||
async listFiles(dirPath: string = "."): Promise<string> {
|
||||
// If the extension is run without a workspace open, we are in the root directory and don't want to list all files since it would prompt for permission to access everything
|
||||
const cwd = process.cwd();
|
||||
const root = process.platform === 'win32' ? path.parse(cwd).root : '/';
|
||||
const cwd = process.cwd()
|
||||
const root = process.platform === "win32" ? path.parse(cwd).root : "/"
|
||||
const isRoot = cwd === root
|
||||
if (isRoot) {
|
||||
return cwd
|
||||
@@ -361,17 +361,20 @@ ${filesInCurrentDir}`
|
||||
// FIXME: instead of using glob to read all files, we will use vscode api to get workspace files list. (otherwise this prompts user to give permissions to read files if e.g. it was opened at root directory)
|
||||
return entries.slice(1, 501).join("\n") // truncate to 500 entries (removes first entry which is the directory itself)
|
||||
} catch (error) {
|
||||
const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}`
|
||||
this.say("error", errorString)
|
||||
const errorString = `Error listing files and directories: ${JSON.stringify(serializeError(error))}`
|
||||
this.say("error", errorString)
|
||||
return errorString
|
||||
}
|
||||
}
|
||||
|
||||
async executeCommand(command: string): Promise<string> {
|
||||
const { response } = await this.ask("command", `Claude wants to execute the following command:\n${command}\nDo you approve?`)
|
||||
if (response !== "yesButtonTapped") {
|
||||
return "Command execution was not approved by the user."
|
||||
}
|
||||
const { response } = await this.ask(
|
||||
"command",
|
||||
`Claude wants to execute the following command:\n${command}\nDo you approve?`
|
||||
)
|
||||
if (response !== "yesButtonTapped") {
|
||||
return "Command execution was not approved by the user."
|
||||
}
|
||||
try {
|
||||
let result = ""
|
||||
// execa by default tries to convery bash into javascript
|
||||
@@ -385,8 +388,8 @@ ${filesInCurrentDir}`
|
||||
} catch (e) {
|
||||
const error = e as any
|
||||
let errorMessage = error.message || JSON.stringify(serializeError(error))
|
||||
const errorString = `Error executing command:\n${errorMessage}`
|
||||
this.say("error", errorString)
|
||||
const errorString = `Error executing command:\n${errorMessage}`
|
||||
this.say("error", errorString)
|
||||
return errorString
|
||||
}
|
||||
}
|
||||
@@ -437,6 +440,7 @@ ${filesInCurrentDir}`
|
||||
}
|
||||
|
||||
try {
|
||||
await this.say("api_req_started", JSON.stringify(userContent))
|
||||
const response = await this.client.messages.create({
|
||||
model: "claude-3-5-sonnet-20240620", // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
max_tokens: 4096,
|
||||
@@ -450,7 +454,7 @@ ${filesInCurrentDir}`
|
||||
let assistantResponses: Anthropic.Messages.ContentBlock[] = []
|
||||
let inputTokens = response.usage.input_tokens
|
||||
let outputTokens = response.usage.output_tokens
|
||||
await this.say("api_cost", `API request cost: ${this.calculateApiCost(inputTokens, outputTokens)}`)
|
||||
await this.say("api_req_finished", this.calculateApiCost(inputTokens, outputTokens).toString())
|
||||
|
||||
// A response always returns text content blocks (it's just that before we were iterating over the completion_attempt response before we could append text response, resulting in bug)
|
||||
for (const contentBlock of response.content) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ExtensionMessage {
|
||||
}
|
||||
|
||||
export interface ClaudeMessage {
|
||||
ts: number
|
||||
type: "ask" | "say"
|
||||
ask?: ClaudeAsk
|
||||
say?: ClaudeSay
|
||||
@@ -16,4 +17,4 @@ export interface ClaudeMessage {
|
||||
}
|
||||
|
||||
export type ClaudeAsk = "request_limit_reached" | "followup" | "command" | "completion_result"
|
||||
export type ClaudeSay = "error" | "api_cost" | "text" | "tool" | "command_output" | "task_completed"
|
||||
export type ClaudeSay = "task" | "error" | "api_req_started" | "api_req_finished" | "text" | "tool" | "command_output"
|
||||
209
webview-ui/src/components/ChatRow.tsx
Normal file
209
webview-ui/src/components/ChatRow.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useState } from "react"
|
||||
import { ClaudeMessage, ClaudeAsk, ClaudeSay } from "@shared/ExtensionMessage"
|
||||
import { VSCodeButton, VSCodeProgressRing, VSCodeTag } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface ChatRowProps {
|
||||
message: ClaudeMessage
|
||||
cost?: string
|
||||
}
|
||||
|
||||
const ChatRow: React.FC<ChatRowProps> = ({ message, cost }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const getIconAndTitle = (type: ClaudeAsk | ClaudeSay | undefined): [JSX.Element | null, string | null] => {
|
||||
switch (type) {
|
||||
case "request_limit_reached":
|
||||
return [
|
||||
<span className="codicon codicon-error" style={{ color: "var(--vscode-errorForeground)" }}></span>,
|
||||
"Max Requests Reached",
|
||||
]
|
||||
case "error":
|
||||
return [
|
||||
<span className="codicon codicon-error" style={{ color: "var(--vscode-errorForeground)" }}></span>,
|
||||
"Error",
|
||||
]
|
||||
case "command":
|
||||
return [<span className="codicon codicon-terminal"></span>, "Command"]
|
||||
case "completion_result":
|
||||
return [
|
||||
<span
|
||||
className="codicon codicon-check"
|
||||
style={{ color: "var(--vscode-testing-iconPassed)" }}></span>,
|
||||
"Task Completed",
|
||||
]
|
||||
case "tool":
|
||||
return [<span className="codicon codicon-tools"></span>, "Tool"]
|
||||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
const [icon, title] = getIconAndTitle(message.type === "ask" ? message.ask : message.say)
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "left",
|
||||
gap: "10px",
|
||||
marginBottom: "10px",
|
||||
}
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
marginLeft: "20px",
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case "say":
|
||||
switch (message.say) {
|
||||
case "task":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-textBlockQuote-background)",
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid var(--vscode-textBlockQuote-border)",
|
||||
}}>
|
||||
<h3 style={headerStyle}>Task</h3>
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
</div>
|
||||
)
|
||||
case "api_req_started":
|
||||
return (
|
||||
<div>
|
||||
<div style={headerStyle}>
|
||||
<span>Made API request...</span>
|
||||
{cost ? (
|
||||
<span
|
||||
className="codicon codicon-check"
|
||||
style={{ color: "var(--vscode-testing-iconPassed)" }}></span>
|
||||
) : (
|
||||
<VSCodeProgressRing />
|
||||
)}
|
||||
{cost && <VSCodeTag>{cost}</VSCodeTag>}
|
||||
</div>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
aria-label="Toggle Details"
|
||||
onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
case "api_req_finished":
|
||||
return null // Hide this message type
|
||||
case "tool":
|
||||
case "error":
|
||||
case "text":
|
||||
case "command_output":
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
)}
|
||||
<pre style={contentStyle}>
|
||||
<code>{message.text}</code>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
)}
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
case "ask":
|
||||
switch (message.ask) {
|
||||
case "request_limit_reached":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
<p style={{ ...contentStyle, color: "var(--vscode-errorForeground)" }}>
|
||||
Your task has reached the maximum request limit (maxRequestsPerTask, you can change
|
||||
this in settings). Do you want to keep going or start a new task?
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
case "command":
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
<div style={contentStyle}>
|
||||
<p>Claude would like to run this command. Do you allow this?</p>
|
||||
<pre>
|
||||
<code>{message.text}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: "5px solid var(--vscode-testing-iconPassed)",
|
||||
paddingLeft: "10px",
|
||||
}}>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4 style={{ color: "var(--vscode-testing-iconPassed)" }}>{title}</h4>
|
||||
</div>
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
<h4>{title}</h4>
|
||||
</div>
|
||||
)}
|
||||
<p style={contentStyle}>{message.text}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message.say === "api_req_finished") {
|
||||
return null // Don't render anything for this message type
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: "10px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
backgroundColor:
|
||||
message.say === "task"
|
||||
? "var(--vscode-textBlockQuote-background)"
|
||||
: "var(--vscode-editor-background)",
|
||||
}}>
|
||||
{renderContent()}
|
||||
{isExpanded && message.say === "api_req_started" && (
|
||||
<pre style={{ marginTop: "10px" }}>
|
||||
<code>{message.text}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatRow
|
||||
@@ -4,12 +4,60 @@ import { KeyboardEvent, useEffect, useRef, useState } from "react"
|
||||
import DynamicTextArea from "react-textarea-autosize"
|
||||
import { vscode } from "../utilities/vscode"
|
||||
import { ClaudeAskResponse } from "@shared/WebviewMessage"
|
||||
import ChatRow from "./ChatRow"
|
||||
|
||||
interface ChatViewProps {
|
||||
messages: ClaudeMessage[]
|
||||
}
|
||||
// maybe instead of storing state in App, just make chatview always show so dont conditionally load/unload? need to make sure messages are persisted (i remember seeing something about how webviews can be frozen in docs)
|
||||
const ChatView = ({ messages}: ChatViewProps) => {
|
||||
const ChatView = ({}: ChatViewProps) => {
|
||||
// dummy data for messages
|
||||
const generateRandomTimestamp = (baseDate: Date, rangeInDays: number): number => {
|
||||
const rangeInMs = rangeInDays * 24 * 60 * 60 * 1000 // convert days to milliseconds
|
||||
const randomOffset = Math.floor(Math.random() * rangeInMs * 2) - rangeInMs // rangeInMs * 2 to have offset in both directions
|
||||
return baseDate.getTime() + randomOffset
|
||||
}
|
||||
|
||||
const baseDate = new Date("2024-07-08T00:00:00Z")
|
||||
|
||||
const messages: ClaudeMessage[] = [
|
||||
{ type: "say", say: "task", text: "type: say, say: task", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{
|
||||
type: "ask",
|
||||
ask: "request_limit_reached",
|
||||
text: "type: ask, ask: request_limit_reached",
|
||||
ts: generateRandomTimestamp(baseDate, 1),
|
||||
},
|
||||
{ type: "ask", ask: "followup", text: "type: ask, ask: followup", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{ type: "ask", ask: "command", text: "type: ask, ask: command", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{ type: "say", say: "error", text: "type: say, say: error", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{
|
||||
type: "say",
|
||||
say: "api_req_started",
|
||||
text: "type: say, say: api_req_started",
|
||||
ts: generateRandomTimestamp(baseDate, 1),
|
||||
},
|
||||
{
|
||||
type: "say",
|
||||
say: "api_req_finished",
|
||||
text: "type: say, say: api_req_finished",
|
||||
ts: generateRandomTimestamp(baseDate, 1),
|
||||
},
|
||||
{ type: "say", say: "text", text: "type: say, say: text", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{ type: "say", say: "tool", text: "type: say, say: tool", ts: generateRandomTimestamp(baseDate, 1) },
|
||||
{
|
||||
type: "say",
|
||||
say: "command_output",
|
||||
text: "type: say, say: command_output",
|
||||
ts: generateRandomTimestamp(baseDate, 1),
|
||||
},
|
||||
{
|
||||
type: "ask",
|
||||
ask: "completion_result",
|
||||
text: "type: ask, ask: completion_result",
|
||||
ts: generateRandomTimestamp(baseDate, 1),
|
||||
},
|
||||
]
|
||||
const [inputValue, setInputValue] = useState("")
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null)
|
||||
@@ -20,14 +68,14 @@ const ChatView = ({ messages}: ChatViewProps) => {
|
||||
|
||||
const scrollToBottom = () => {
|
||||
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: 'nearest', inline: 'start' })
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
// if last message is an ask, show user ask UI
|
||||
|
||||
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
||||
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
|
||||
// basically as long as a task is active, the conversation history will be persisted
|
||||
|
||||
const lastMessage = messages.at(-1)
|
||||
@@ -42,14 +90,12 @@ const ChatView = ({ messages}: ChatViewProps) => {
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
|
||||
const handleSendMessage = () => {
|
||||
const text = inputValue.trim()
|
||||
if (text) {
|
||||
setInputValue("")
|
||||
if (messages.length === 0) {
|
||||
|
||||
vscode.postMessage({ type: "newTask", text })
|
||||
vscode.postMessage({ type: "newTask", text })
|
||||
} else if (claudeAsk) {
|
||||
switch (claudeAsk) {
|
||||
case "followup":
|
||||
@@ -81,28 +127,23 @@ const ChatView = ({ messages}: ChatViewProps) => {
|
||||
if (textAreaRef.current && !textAreaHeight) {
|
||||
setTextAreaHeight(textAreaRef.current.offsetHeight)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100vh", overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
backgroundColor: "gray",
|
||||
}}>
|
||||
<div style={{ flexGrow: 1, overflowY: "scroll", scrollbarWidth: "none" }}>
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
marginBottom: "10px",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor:
|
||||
message.type === "ask"
|
||||
? "var(--vscode-editor-background)"
|
||||
: "var(--vscode-sideBar-background)",
|
||||
}}>
|
||||
<span style={{ whiteSpace: "pre-line", overflowWrap: "break-word" }}>{message.text}</span>
|
||||
</div>
|
||||
{messages.map((message) => (
|
||||
<ChatRow message={message} />
|
||||
))}
|
||||
<div style={{ float:"left", clear: "both" }} ref={messagesEndRef} />
|
||||
<div style={{ float: "left", clear: "both" }} ref={messagesEndRef} />
|
||||
</div>
|
||||
<div style={{ position: "relative", paddingTop: "16px", paddingBottom: "16px" }}>
|
||||
<DynamicTextArea
|
||||
|
||||
Reference in New Issue
Block a user