Add task history

This commit is contained in:
Saoud Rizwan
2024-08-17 08:29:30 -04:00
parent 38f98951d0
commit d1437e6d2d
18 changed files with 1211 additions and 137 deletions

View File

@@ -1,12 +1,13 @@
// type that represents json data that is sent from extension to webview, called ExtensionMessage and has 'type' enum which can be 'plusButtonTapped' or 'settingsButtonTapped' or 'hello'
import { ApiConfiguration } from "./api"
import { HistoryItem } from "./HistoryItem"
// webview will hold state
export interface ExtensionMessage {
type: "action" | "state" | "selectedImages"
text?: string
action?: "plusButtonTapped" | "settingsButtonTapped" | "didBecomeVisible"
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
state?: ExtensionState
images?: string[]
}
@@ -18,6 +19,7 @@ export interface ExtensionState {
customInstructions?: string
themeName?: string
claudeMessages: ClaudeMessage[]
taskHistory: HistoryItem[]
shouldShowAnnouncement: boolean
}
@@ -38,6 +40,7 @@ export type ClaudeAsk =
| "completion_result"
| "tool"
| "api_req_failed"
| "resume_task"
export type ClaudeSay =
| "task"

10
src/shared/HistoryItem.ts Normal file
View File

@@ -0,0 +1,10 @@
export type HistoryItem = {
id: string
ts: number
task: string
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
}

View File

@@ -10,8 +10,11 @@ export interface WebviewMessage {
| "askResponse"
| "clearTask"
| "didShowAnnouncement"
| "downloadTask"
| "selectImages"
| "exportCurrentTask"
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
text?: string
askResponse?: ClaudeAskResponse
apiConfiguration?: ApiConfiguration

View File

@@ -0,0 +1,62 @@
import { ClaudeMessage } from "./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
})
}

View File

@@ -0,0 +1,73 @@
import { ClaudeMessage } from "./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: 'ask', ask: 'command_output', text: 'file1.txt', ts: 1625097601000 },
* { type: 'ask', ask: '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 didAddOutput = false
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].ask === "command_output" || messages[j].say === "command_output") {
if (!didAddOutput) {
// Add a newline before the first output
combinedText += `\n${COMMAND_OUTPUT_STRING}`
didAddOutput = true
}
// handle cases where we receive empty command_output (ie when extension is relinquishing control over exit command button)
const output = messages[j].text || ""
if (output.length > 0) {
combinedText += "\n" + output
}
}
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.ask === "command_output" || 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
})
}
export const COMMAND_OUTPUT_STRING = "Output:"

View File

@@ -0,0 +1,65 @@
import { ClaudeMessage } from "./ExtensionMessage"
interface ApiMetrics {
totalTokensIn: number
totalTokensOut: number
totalCacheWrites?: number
totalCacheReads?: number
totalCost: number
}
/**
* Calculates API metrics from an array of ClaudeMessages.
*
* This function processes 'api_req_started' messages that have been combined with their
* corresponding 'api_req_finished' messages by the combineApiRequests function.
* It extracts and sums up the tokensIn, tokensOut, cacheWrites, cacheReads, and cost from these messages.
*
* @param messages - An array of ClaudeMessage objects to process.
* @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, and totalCost.
*
* @example
* const messages = [
* { type: "say", say: "api_req_started", text: '{"request":"GET /api/data","tokensIn":10,"tokensOut":20,"cost":0.005}', ts: 1000 }
* ];
* const { totalTokensIn, totalTokensOut, totalCost } = getApiMetrics(messages);
* // Result: { totalTokensIn: 10, totalTokensOut: 20, totalCost: 0.005 }
*/
export function getApiMetrics(messages: ClaudeMessage[]): ApiMetrics {
const result: ApiMetrics = {
totalTokensIn: 0,
totalTokensOut: 0,
totalCacheWrites: undefined,
totalCacheReads: undefined,
totalCost: 0,
}
messages.forEach((message) => {
if (message.type === "say" && message.say === "api_req_started" && message.text) {
try {
const parsedData = JSON.parse(message.text)
const { tokensIn, tokensOut, cacheWrites, cacheReads, cost } = parsedData
if (typeof tokensIn === "number") {
result.totalTokensIn += tokensIn
}
if (typeof tokensOut === "number") {
result.totalTokensOut += tokensOut
}
if (typeof cacheWrites === "number") {
result.totalCacheWrites = (result.totalCacheWrites ?? 0) + cacheWrites
}
if (typeof cacheReads === "number") {
result.totalCacheReads = (result.totalCacheReads ?? 0) + cacheReads
}
if (typeof cost === "number") {
result.totalCost += cost
}
} catch (error) {
console.error("Error parsing JSON:", error)
}
}
})
return result
}