From a0a533d72e7b3358a3d5ef685799c44d1e6ffd89 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:09:56 -0400 Subject: [PATCH] Add combineCommandSequences and combineApiRequests to group certain message types --- webview-ui/src/components/ChatRow.tsx | 13 ++-- webview-ui/src/components/ChatView.tsx | 73 +++++++++++++------ .../src/utilities/combineApiRequests.ts | 62 ++++++++++++++++ .../src/utilities/combineCommandSequences.ts | 62 ++++++++++++++++ 4 files changed, 181 insertions(+), 29 deletions(-) create mode 100644 webview-ui/src/utilities/combineApiRequests.ts create mode 100644 webview-ui/src/utilities/combineCommandSequences.ts diff --git a/webview-ui/src/components/ChatRow.tsx b/webview-ui/src/components/ChatRow.tsx index f7917e0..fae52e4 100644 --- a/webview-ui/src/components/ChatRow.tsx +++ b/webview-ui/src/components/ChatRow.tsx @@ -81,13 +81,14 @@ const ChatRow: React.FC = ({ message, cost }) => { )} {cost && {cost}} + setIsExpanded(!isExpanded)}> + + - setIsExpanded(!isExpanded)}> - - ) case "api_req_finished": diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index 2b97876..9b1b2ff 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -1,10 +1,12 @@ import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "@shared/ExtensionMessage" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" -import { KeyboardEvent, useEffect, useRef, useState } from "react" +import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react" import DynamicTextArea from "react-textarea-autosize" import { vscode } from "../utilities/vscode" import { ClaudeAskResponse } from "@shared/WebviewMessage" import ChatRow from "./ChatRow" +import { combineCommandSequences } from "../utilities/combineCommandSequences" +import { combineApiRequests } from "../utilities/combineApiRequests" interface ChatViewProps { messages: ClaudeMessage[] @@ -21,43 +23,68 @@ const ChatView = ({}: ChatViewProps) => { 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: "say", say: "task", text: "Starting task", ts: generateRandomTimestamp(baseDate, 1) }, { type: "ask", ask: "request_limit_reached", - text: "type: ask, ask: request_limit_reached", - ts: generateRandomTimestamp(baseDate, 1), + text: "Request limit reached", + ts: generateRandomTimestamp(baseDate, 2), }, - { 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: "ask", ask: "followup", text: "Any additional questions?", ts: generateRandomTimestamp(baseDate, 3) }, + { type: "say", say: "error", text: "An error occurred", ts: generateRandomTimestamp(baseDate, 4) }, + + { type: "say", say: "text", text: "Some general text", ts: generateRandomTimestamp(baseDate, 7) }, + { type: "say", say: "tool", text: "Using a tool", ts: generateRandomTimestamp(baseDate, 8) }, + + // First command sequence + { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 9) }, + { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 10) }, + { type: "say", say: "api_req_started", text: JSON.stringify({ request: "GET /api/data" }), ts: generateRandomTimestamp(baseDate, 5) }, + { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 11) }, + { type: "say", say: "command_output", text: "directory1", ts: generateRandomTimestamp(baseDate, 12) }, + + { type: "say", say: "text", text: "Interrupting text", ts: generateRandomTimestamp(baseDate, 13) }, + { type: "say", say: "api_req_finished", text: JSON.stringify({ cost: "GET /api/data" }), ts: generateRandomTimestamp(baseDate, 6) }, + // Second command sequence + { type: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 14) }, + { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 15) }, + + { type: "ask", ask: "completion_result", text: "Task completed", ts: generateRandomTimestamp(baseDate, 16) }, + + // Third command sequence (no output) + { type: "ask", ask: "command", text: "echo Hello", ts: generateRandomTimestamp(baseDate, 17) }, + + // Testing combineApiRequests + { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 18) }, + { type: "ask", ask: "command", text: "ls -l", ts: generateRandomTimestamp(baseDate, 19) }, + { type: "say", say: "command_output", text: "file1.txt", ts: generateRandomTimestamp(baseDate, 20) }, { type: "say", say: "api_req_started", - text: "type: say, say: api_req_started", - ts: generateRandomTimestamp(baseDate, 1), + text: JSON.stringify({ request: "GET /api/data" }), + ts: generateRandomTimestamp(baseDate, 23), }, + { type: "say", say: "command_output", text: "file2.txt", ts: generateRandomTimestamp(baseDate, 24) }, + { type: "say", say: "text", text: "Some random text", ts: generateRandomTimestamp(baseDate, 25) }, { type: "say", say: "api_req_finished", - text: "type: say, say: api_req_finished", - ts: generateRandomTimestamp(baseDate, 1), + text: JSON.stringify({ cost: 0.005 }), + ts: generateRandomTimestamp(baseDate, 26), }, - { 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: "ask", ask: "command", text: "pwd", ts: generateRandomTimestamp(baseDate, 27) }, + { type: "say", say: "command_output", text: "/home/user", ts: generateRandomTimestamp(baseDate, 28) }, { 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), + say: "api_req_started", + text: JSON.stringify({ request: "POST /api/update" }), + ts: generateRandomTimestamp(baseDate, 29), }, + { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) }, ] + + const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages)), [messages]) + const [inputValue, setInputValue] = useState("") const messagesEndRef = useRef(null) const textAreaRef = useRef(null) @@ -140,7 +167,7 @@ const ChatView = ({}: ChatViewProps) => { backgroundColor: "gray", }}>
- {messages.map((message) => ( + {modifiedMessages.map((message) => ( ))}
diff --git a/webview-ui/src/utilities/combineApiRequests.ts b/webview-ui/src/utilities/combineApiRequests.ts new file mode 100644 index 0000000..d540818 --- /dev/null +++ b/webview-ui/src/utilities/combineApiRequests.ts @@ -0,0 +1,62 @@ +import { ClaudeMessage } from "@shared/ExtensionMessage" + +/** + * Combines API request start and finish messages in an array of ClaudeMessages. + * + * This function looks for pairs of 'api_req_started' and 'api_req_finished' messages. + * When it finds a pair, it combines them into a single 'api_req_combined' message. + * The JSON data in the text fields of both messages are merged. + * + * @param messages - An array of ClaudeMessage objects to process. + * @returns A new array of ClaudeMessage objects with API requests combined. + * + * @example + * const messages = [ + * { type: "say", say: "api_req_started", text: '{"request":"GET /api/data"}', ts: 1000 }, + * { type: "say", say: "api_req_finished", text: '{"cost":0.005}', ts: 1001 } + * ]; + * const result = combineApiRequests(messages); + * // Result: [{ type: "say", say: "api_req_started", text: '{"request":"GET /api/data","cost":0.005}', ts: 1000 }] + */ +export function combineApiRequests(messages: ClaudeMessage[]): ClaudeMessage[] { + const combinedApiRequests: ClaudeMessage[] = [] + + for (let i = 0; i < messages.length; i++) { + if (messages[i].type === "say" && messages[i].say === "api_req_started") { + let startedRequest = JSON.parse(messages[i].text || "{}") + let j = i + 1 + + while (j < messages.length) { + if (messages[j].type === "say" && messages[j].say === "api_req_finished") { + let finishedRequest = JSON.parse(messages[j].text || "{}") + let combinedRequest = { ...startedRequest, ...finishedRequest } + + combinedApiRequests.push({ + ...messages[i], + text: JSON.stringify(combinedRequest), + }) + + i = j // Skip to the api_req_finished message + break + } + j++ + } + + if (j === messages.length) { + // If no matching api_req_finished found, keep the original api_req_started + combinedApiRequests.push(messages[i]) + } + } + } + + // Replace original api_req_started and remove api_req_finished + return messages + .filter((msg) => !(msg.type === "say" && msg.say === "api_req_finished")) + .map((msg) => { + if (msg.type === "say" && msg.say === "api_req_started") { + const combinedRequest = combinedApiRequests.find((req) => req.ts === msg.ts) + return combinedRequest || msg + } + return msg + }) +} diff --git a/webview-ui/src/utilities/combineCommandSequences.ts b/webview-ui/src/utilities/combineCommandSequences.ts new file mode 100644 index 0000000..3259dbc --- /dev/null +++ b/webview-ui/src/utilities/combineCommandSequences.ts @@ -0,0 +1,62 @@ +import { ClaudeMessage } from "@shared/ExtensionMessage" + +/** + * Combines sequences of command and command_output messages in an array of ClaudeMessages. + * + * This function processes an array of ClaudeMessage objects, looking for sequences + * where a 'command' message is followed by one or more 'command_output' messages. + * When such a sequence is found, it combines them into a single message, merging + * their text contents. + * + * @param messages - An array of ClaudeMessage objects to process. + * @returns A new array of ClaudeMessage objects with command sequences combined. + * + * @example + * const messages: ClaudeMessage[] = [ + * { type: 'ask', ask: 'command', text: 'ls', ts: 1625097600000 }, + * { type: 'say', say: 'command_output', text: 'file1.txt', ts: 1625097601000 }, + * { type: 'say', say: 'command_output', text: 'file2.txt', ts: 1625097602000 } + * ]; + * const result = simpleCombineCommandSequences(messages); + * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }] + */ +export function combineCommandSequences(messages: ClaudeMessage[]): ClaudeMessage[] { + const combinedCommands: ClaudeMessage[] = [] + + // First pass: combine commands with their outputs + for (let i = 0; i < messages.length; i++) { + if (messages[i].type === "ask" && messages[i].ask === "command") { + let combinedText = messages[i].text || "" + let j = i + 1 + + while (j < messages.length) { + if (messages[j].type === "ask" && messages[j].ask === "command") { + // Stop if we encounter the next command + break + } + if (messages[j].type === "say" && messages[j].say === "command_output") { + combinedText += "\n" + (messages[j].text || "") + } + j++ + } + + combinedCommands.push({ + ...messages[i], + text: combinedText, + }) + + i = j - 1 // Move to the index just before the next command or end of array + } + } + + // Second pass: remove command_outputs and replace original commands with combined ones + return messages + .filter((msg) => !(msg.type === "say" && msg.say === "command_output")) + .map((msg) => { + if (msg.type === "ask" && msg.ask === "command") { + const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts) + return combinedCommand || msg + } + return msg + }) +}