Add tool message types; show tool specific information in webview; separate command from output; add abort button to task card

This commit is contained in:
Saoud Rizwan
2024-07-09 15:05:29 -04:00
parent 771c612d8a
commit d63aef015a
7 changed files with 191 additions and 74 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState } from "react"
import { ClaudeMessage, ClaudeAsk, ClaudeSay } from "@shared/ExtensionMessage"
import { ClaudeMessage, ClaudeAsk, ClaudeSay, ClaudeSayTool } from "@shared/ExtensionMessage"
import { VSCodeButton, VSCodeProgressRing, VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
import { COMMAND_OUTPUT_STRING } from "../utilities/combineCommandSequences"
interface ChatRowProps {
message: ClaudeMessage
@@ -92,6 +93,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
const contentStyle: React.CSSProperties = {
margin: 0,
whiteSpace: "pre-line",
}
switch (message.type) {
@@ -116,18 +118,58 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
case "api_req_finished":
return null // Hide this message type
case "tool":
//const tool = JSON.parse(message.text || "{}") as ClaudeSayTool
const tool: ClaudeSayTool = {
tool: "editedExistingFile",
path: "/path/to/file",
}
switch (tool.tool) {
case "editedExistingFile":
return (
<>
<div style={headerStyle}>
{icon}
Edited File
</div>
<p>Path: {tool.path!}</p>
<p>{tool.diff!}</p>
</>
)
case "newFileCreated":
return (
<>
<div style={headerStyle}>
{icon}
Created New File
</div>
<p>Path: {tool.path!}</p>
<p>{tool.content!}</p>
</>
)
case "readFile":
return (
<>
<div style={headerStyle}>
{icon}
Read File
</div>
<p>Path: {tool.path!}</p>
</>
)
case "listFiles":
return (
<>
<div style={headerStyle}>
{icon}
Viewed Directory
</div>
<p>Path: {tool.path!}</p>
</>
)
}
break
case "text":
return (
<>
{title && (
<div style={headerStyle}>
{icon}
{title}
</div>
)}
<p style={contentStyle}>{message.text}</p>
</>
)
return <p style={contentStyle}>{message.text}</p>
case "error":
return (
<>
@@ -167,6 +209,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
</>
)
}
break
case "ask":
switch (message.ask) {
case "request_limit_reached":
@@ -177,12 +220,23 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
{title}
</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?
{message.text}
</p>
</>
)
case "command":
const splitMessage = (text: string) => {
const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
if (outputIndex === -1) {
return { command: text, output: "" }
}
return {
command: text.slice(0, outputIndex).trim(),
output: text.slice(outputIndex + COMMAND_OUTPUT_STRING.length).trim(),
}
}
const { command, output } = splitMessage(message.text || "")
return (
<>
<div style={headerStyle}>
@@ -190,10 +244,16 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
{title}
</div>
<div style={contentStyle}>
<p>Claude would like to run this command. Do you allow this?</p>
<pre style={contentStyle}>
<code>{message.text}</code>
</pre>
<p style={contentStyle}>Claude Dev wants to execute the following command:</p>
<p style={contentStyle}>{command}</p>
{output && (
<>
<p style={{ ...contentStyle, fontWeight: "bold" }}>
{COMMAND_OUTPUT_STRING}
</p>
<p style={contentStyle}>{output}</p>
</>
)}
</div>
</>
)
@@ -240,9 +300,7 @@ const ChatRow: React.FC<ChatRowProps> = ({ message }) => {
}}>
{renderContent()}
{isExpanded && message.say === "api_req_started" && (
<pre style={{ marginTop: "10px" }}>
<code>{message.text}</code>
</pre>
<p style={{ marginTop: "10px" }}>{JSON.stringify(JSON.parse(message.text || "{}").request)}</p>
)}
</div>
)

View File

@@ -33,7 +33,11 @@ const ChatView = ({ messages }: ChatViewProps) => {
const scrollToBottom = (instant: boolean = false) => {
// https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
(messagesEndRef.current as any)?.scrollIntoView({ behavior: instant ? "instant" : "smooth", block: "nearest", inline: "start" })
;(messagesEndRef.current as any)?.scrollIntoView({
behavior: instant ? "instant" : "smooth",
block: "nearest",
inline: "start",
})
}
const handlePrimaryButtonClick = () => {
@@ -49,8 +53,19 @@ const ChatView = ({ messages }: ChatViewProps) => {
setSecondaryButtonText(undefined)
}
// scroll to bottom when new message is added
const visibleMessages = useMemo(
() =>
modifiedMessages.filter(
(message) => !(message.type === "ask" && message.ask === "completion_result" && message.text === "")
),
[modifiedMessages]
)
useEffect(() => {
scrollToBottom()
}, [visibleMessages.length])
useEffect(() => {
// 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.
@@ -110,6 +125,10 @@ const ChatView = ({ messages }: ChatViewProps) => {
}
}
const handleTaskCloseButtonClick = () => {
vscode.postMessage({ type: "abortTask" })
}
useEffect(() => {
if (textAreaRef.current && !textAreaHeight) {
setTextAreaHeight(textAreaRef.current.offsetHeight)
@@ -158,6 +177,7 @@ const ChatView = ({ messages }: ChatViewProps) => {
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
totalCost={apiMetrics.totalCost}
onClose={handleTaskCloseButtonClick}
/>
<div
className="scrollable"

View File

@@ -1,32 +1,45 @@
import React, { useState } from "react"
import TextTruncate from "react-text-truncate"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
interface TaskHeaderProps {
taskText: string
tokensIn: number
tokensOut: number
totalCost: number
onClose: () => void
}
const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut, totalCost }) => {
const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut, totalCost, onClose }) => {
const [isExpanded, setIsExpanded] = useState(false)
const toggleExpand = () => setIsExpanded(!isExpanded)
return (
<div
style={{
padding: "15px 15px 10px 15px",
}}>
<div style={{ padding: "15px 15px 10px 15px" }}>
<div
style={{
backgroundColor: "var(--vscode-badge-background)",
color: "var(--vscode-badge-foreground)",
borderRadius: "3px",
padding: "8px",
padding: "12px",
display: "flex",
flexDirection: "column",
gap: "8px",
}}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span style={{ fontWeight: "bold", fontSize: "16px" }}>Task</span>
<VSCodeButton
appearance="icon"
onClick={onClose}
style={{ marginTop: "-5px", marginRight: "-5px" }}>
<span className="codicon codicon-close"></span>
</VSCodeButton>
</div>
<div style={{ fontSize: "var(--vscode-font-size)", lineHeight: "1.5" }}>
<TextTruncate
line={isExpanded ? 0 : 3}
@@ -58,20 +71,24 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({ taskText, tokensIn, tokensOut,
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>Tokens:</span>
<div style={{ display: "flex", gap: "8px" }}>
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<i className="codicon codicon-arrow-down" style={{ fontSize: "12px", marginBottom: "-1px" }} />
{tokensIn.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<i className="codicon codicon-arrow-up" style={{ fontSize: "12px", marginBottom: "-1px" }} />
{tokensOut.toLocaleString()}
</span>
</div>
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<i
className="codicon codicon-arrow-down"
style={{ fontSize: "12px", marginBottom: "-2px" }}
/>
{tokensIn.toLocaleString()}
</span>
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<i
className="codicon codicon-arrow-up"
style={{ fontSize: "12px", marginBottom: "-2px" }}
/>
{tokensOut.toLocaleString()}
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<span style={{ fontWeight: "bold" }}>API Cost:</span>
<span>${totalCost.toFixed(4)}</span>
</div>

View File

@@ -27,6 +27,7 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
for (let i = 0; i < messages.length; i++) {
if (messages[i].type === "ask" && messages[i].ask === "command") {
let combinedText = messages[i].text || ""
let didAddOutput = false
let j = i + 1
while (j < messages.length) {
@@ -35,6 +36,11 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
break
}
if (messages[j].type === "say" && messages[j].say === "command_output") {
if (!didAddOutput) {
// Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}`
didAddOutput = true
}
combinedText += "\n" + (messages[j].text || "")
}
j++
@@ -60,3 +66,4 @@ export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessag
return msg
})
}
export const COMMAND_OUTPUT_STRING = "Output:"