Browse Source

Focus textarea when webview becomes visible; add getApiMetrics

Saoud Rizwan 1 year ago
parent
commit
771c612d8a

+ 11 - 0
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()
 	}

+ 1 - 1
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[] }
 }
 

+ 81 - 2
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 (

+ 31 - 4
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<HTMLDivElement>(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) => {
 			}}>
 			<TaskHeader
 				taskText={task?.text || ""}
-				tokensIn={1000}
-				tokensOut={1500}
-				totalCost={0.0025}
+				tokensIn={apiMetrics.totalTokensIn}
+				tokensOut={apiMetrics.totalTokensOut}
+				totalCost={apiMetrics.totalCost}
 			/>
 			<div
 				className="scrollable"
@@ -177,6 +203,7 @@ const ChatView = ({ messages }: ChatViewProps) => {
 					onHeightChange={() => scrollToBottom(true)}
 					placeholder="Type a message..."
 					maxRows={10}
+					autoFocus={true}
 					style={{
 						width: "100%",
 						boxSizing: "border-box",

+ 55 - 0
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
+}