diff --git a/src/providers/SidebarProvider.ts b/src/providers/SidebarProvider.ts index c7f8356..6002dae 100644 --- a/src/providers/SidebarProvider.ts +++ b/src/providers/SidebarProvider.ts @@ -41,6 +41,17 @@ export class SidebarProvider implements vscode.WebviewViewProvider { // and executes code based on the message that is recieved this.setWebviewMessageListener(webviewView.webview) + // Listen for when the panel becomes visible + // https://github.com/microsoft/vscode-discussions/discussions/840 + webviewView.onDidChangeVisibility((e: any) => { + if (e.visible) { + // Your view is visible + this.postMessageToWebview({ type: "action", action: "didBecomeVisible"}) + } else { + // Your view is hidden + } + }) + // if the extension is starting a new session, clear previous task state this.resetTask() } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 691e4c1..1e5b0e4 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -4,7 +4,7 @@ export interface ExtensionMessage { type: "action" | "state" text?: string - action?: "plusButtonTapped" | "settingsButtonTapped" + action?: "plusButtonTapped" | "settingsButtonTapped" | "didBecomeVisible" state?: { didOpenOnce: boolean, apiKey?: string, maxRequestsPerTask?: number, claudeMessages: ClaudeMessage[] } } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index f6a00ce..ac949ba 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -24,7 +24,8 @@ const App: React.FC = () => { useEffect(() => { vscode.postMessage({ type: "webviewDidLaunch" }) - window.addEventListener("message", (e: MessageEvent) => { + + const handleMessage = (e: MessageEvent) => { const message: ExtensionMessage = e.data // switch message.type switch (message.type) { @@ -50,7 +51,13 @@ const App: React.FC = () => { } break } - }) + } + + window.addEventListener("message", handleMessage) + + return () => { + window.removeEventListener("message", handleMessage) + } }, []) // dummy data for messages @@ -136,6 +143,78 @@ const App: React.FC = () => { ts: generateRandomTimestamp(baseDate, 29), }, { type: "say", say: "text", text: "Final message", ts: generateRandomTimestamp(baseDate, 30) }, + { + type: "say", + say: "text", + text: "Starting API requests", + ts: Date.now(), + }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: "GET /api/data1", + }), + ts: Date.now() + 1000, + }, + { + type: "say", + say: "api_req_finished", + text: JSON.stringify({ + tokensIn: 10, + tokensOut: 20, + cost: 0.002, + }), + ts: Date.now() + 2000, + }, + { + type: "say", + say: "text", + text: "Processing data...", + ts: Date.now() + 3000, + }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: "POST /api/update1", + }), + ts: Date.now() + 4000, + }, + { + type: "say", + say: "api_req_finished", + text: JSON.stringify({ + tokensIn: 15, + tokensOut: 25, + cost: 0.003, + }), + ts: Date.now() + 5000, + }, + { + type: "say", + say: "text", + text: "More processing...", + ts: Date.now() + 6000, + }, + { + type: "say", + say: "api_req_started", + text: JSON.stringify({ + request: "GET /api/data2", + }), + ts: Date.now() + 7000, + }, + { + type: "say", + say: "api_req_finished", + text: JSON.stringify({ + tokensIn: 5, + tokensOut: 15, + cost: 0.001, + }), + ts: Date.now() + 8000, + }, ] return ( diff --git a/webview-ui/src/components/ChatView.tsx b/webview-ui/src/components/ChatView.tsx index ce68cf3..6a2408d 100644 --- a/webview-ui/src/components/ChatView.tsx +++ b/webview-ui/src/components/ChatView.tsx @@ -8,6 +8,7 @@ import ChatRow from "./ChatRow" import { combineCommandSequences } from "../utilities/combineCommandSequences" import { combineApiRequests } from "../utilities/combineApiRequests" import TaskHeader from "./TaskHeader" +import { getApiMetrics } from "../utilities/getApiMetrics" interface ChatViewProps { messages: ClaudeMessage[] @@ -16,6 +17,8 @@ interface ChatViewProps { const ChatView = ({ messages }: ChatViewProps) => { const task = messages.shift() const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages)), [messages]) + // has to be after api_req_finished are all reduced into api_req_started messages + const apiMetrics = useMemo(() => getApiMetrics(modifiedMessages), [modifiedMessages]) const [inputValue, setInputValue] = useState("") const messagesEndRef = useRef(null) @@ -110,7 +113,30 @@ const ChatView = ({ messages }: ChatViewProps) => { useEffect(() => { if (textAreaRef.current && !textAreaHeight) { setTextAreaHeight(textAreaRef.current.offsetHeight) - textAreaRef.current.focus() + //textAreaRef.current.focus() + } + + const handleMessage = (e: MessageEvent) => { + const message: ExtensionMessage = e.data + switch (message.type) { + case "action": + switch (message.action!) { + case "didBecomeVisible": + textAreaRef.current?.focus() + break + } + break + } + } + + window.addEventListener("message", handleMessage) + + const timer = setTimeout(() => { + textAreaRef.current?.focus() + }, 20) + return () => { + clearTimeout(timer) + window.removeEventListener("message", handleMessage) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -129,9 +155,9 @@ const ChatView = ({ messages }: ChatViewProps) => { }}>
{ onHeightChange={() => scrollToBottom(true)} placeholder="Type a message..." maxRows={10} + autoFocus={true} style={{ width: "100%", boxSizing: "border-box", diff --git a/webview-ui/src/utilities/getApiMetrics.ts b/webview-ui/src/utilities/getApiMetrics.ts new file mode 100644 index 0000000..197ab41 --- /dev/null +++ b/webview-ui/src/utilities/getApiMetrics.ts @@ -0,0 +1,55 @@ +import { ClaudeMessage } from "@shared/ExtensionMessage" + +interface ApiMetrics { + totalTokensIn: number + totalTokensOut: 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, and cost from these messages. + * + * @param messages - An array of ClaudeMessage objects to process. + * @returns An ApiMetrics object containing totalTokensIn, totalTokensOut, 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, + 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, cost } = parsedData + + if (typeof tokensIn === "number") { + result.totalTokensIn += tokensIn + } + if (typeof tokensOut === "number") { + result.totalTokensOut += tokensOut + } + if (typeof cost === "number") { + result.totalCost += cost + } + } catch (error) { + console.error("Error parsing JSON:", error) + } + } + }) + + return result +}