From 83dcb0c7f406cdcac911a60df87247c3a9e0dd10 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 12:57:37 -0700 Subject: [PATCH 1/5] split up big aimessage.tsx file, implement feedback/copy buttons UI. --- frontend/app/aipanel/aifeedbackbuttons.tsx | 78 +++++ frontend/app/aipanel/aimessage.tsx | 377 +++------------------ frontend/app/aipanel/aitooluse.tsx | 297 ++++++++++++++++ frontend/app/element/emojibutton.tsx | 30 +- 4 files changed, 450 insertions(+), 332 deletions(-) create mode 100644 frontend/app/aipanel/aifeedbackbuttons.tsx create mode 100644 frontend/app/aipanel/aitooluse.tsx diff --git a/frontend/app/aipanel/aifeedbackbuttons.tsx b/frontend/app/aipanel/aifeedbackbuttons.tsx new file mode 100644 index 0000000000..c3b9f35539 --- /dev/null +++ b/frontend/app/aipanel/aifeedbackbuttons.tsx @@ -0,0 +1,78 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { cn, makeIconClass } from "@/util/util"; +import { memo, useState } from "react"; + +interface AIFeedbackButtonsProps { + messageText: string; +} + +export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) => { + const [thumbsUpClicked, setThumbsUpClicked] = useState(false); + const [thumbsDownClicked, setThumbsDownClicked] = useState(false); + const [copied, setCopied] = useState(false); + + const handleThumbsUp = () => { + setThumbsUpClicked(!thumbsUpClicked); + if (thumbsDownClicked) { + setThumbsDownClicked(false); + } + }; + + const handleThumbsDown = () => { + setThumbsDownClicked(!thumbsDownClicked); + if (thumbsUpClicked) { + setThumbsUpClicked(false); + } + }; + + const handleCopy = () => { + navigator.clipboard.writeText(messageText); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+ + + +
+ ); +}); + +AIFeedbackButtons.displayName = "AIFeedbackButtons"; \ No newline at end of file diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index 95d3f9c28a..7c187d1ae9 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -1,51 +1,54 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { BlockModel } from "@/app/block/block-model"; import { WaveStreamdown } from "@/app/element/streamdown"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; import { cn } from "@/util/util"; -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useEffect, useRef } from "react"; import { getFileIcon } from "./ai-utils"; +import { AIFeedbackButtons } from "./aifeedbackbuttons"; +import { AIToolUseGroup } from "./aitooluse"; import { WaveUIMessage, WaveUIMessagePart } from "./aitypes"; import { WaveAIModel } from "./waveai-model"; -const AIThinking = memo(({ message = "AI is thinking...", reasoningText }: { message?: string; reasoningText?: string }) => { - const scrollRef = useRef(null); +const AIThinking = memo( + ({ message = "AI is thinking...", reasoningText }: { message?: string; reasoningText?: string }) => { + const scrollRef = useRef(null); - useEffect(() => { - if (scrollRef.current && reasoningText) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [reasoningText]); - - const displayText = reasoningText ? (() => { - const lastDoubleNewline = reasoningText.lastIndexOf("\n\n"); - return lastDoubleNewline !== -1 ? reasoningText.substring(lastDoubleNewline + 2) : reasoningText; - })() : ""; - - return ( -
-
-
- - - + useEffect(() => { + if (scrollRef.current && reasoningText) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [reasoningText]); + + const displayText = reasoningText + ? (() => { + const lastDoubleNewline = reasoningText.lastIndexOf("\n\n"); + return lastDoubleNewline !== -1 ? reasoningText.substring(lastDoubleNewline + 2) : reasoningText; + })() + : ""; + + return ( +
+
+
+ + + +
+ {message && {message}}
- {message && {message}} + {displayText && ( +
+ {displayText} +
+ )}
- {displayText && ( -
- {displayText} -
- )} -
- ); -}); + ); + } +); AIThinking.displayName = "AIThinking"; @@ -94,294 +97,6 @@ const UserMessageFiles = memo(({ fileParts }: UserMessageFilesProps) => { UserMessageFiles.displayName = "UserMessageFiles"; -interface AIToolApprovalButtonsProps { - count: number; - onApprove: () => void; - onDeny: () => void; -} - -const AIToolApprovalButtons = memo(({ count, onApprove, onDeny }: AIToolApprovalButtonsProps) => { - const approveText = count > 1 ? `Approve All (${count})` : "Approve"; - const denyText = count > 1 ? "Deny All" : "Deny"; - - return ( -
- - -
- ); -}); - -AIToolApprovalButtons.displayName = "AIToolApprovalButtons"; - -interface AIToolUseBatchItemProps { - part: WaveUIMessagePart & { type: "data-tooluse" }; - effectiveApproval: string; -} - -const AIToolUseBatchItem = memo(({ part, effectiveApproval }: AIToolUseBatchItemProps) => { - const statusIcon = part.data.status === "completed" ? "✓" : part.data.status === "error" ? "✗" : "•"; - const statusColor = - part.data.status === "completed" - ? "text-success" - : part.data.status === "error" - ? "text-error" - : "text-gray-400"; - const effectiveErrorMessage = part.data.errormessage || (effectiveApproval === "timeout" ? "Not approved" : null); - - return ( -
- {statusIcon} -
- {part.data.tooldesc} - {effectiveErrorMessage &&
{effectiveErrorMessage}
} -
-
- ); -}); - -AIToolUseBatchItem.displayName = "AIToolUseBatchItem"; - -interface AIToolUseBatchProps { - parts: Array; // parts must not be empty - isStreaming: boolean; -} - -const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { - const [userApprovalOverride, setUserApprovalOverride] = useState(null); - - const firstTool = parts[0].data; - const baseApproval = userApprovalOverride || firstTool.approval; - const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; - const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); - - useEffect(() => { - if (!isStreaming || effectiveApproval !== "needs-approval") return; - - const interval = setInterval(() => { - parts.forEach((part) => { - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: part.data.toolcallid, - keepalive: true, - }); - }); - }, 4000); - - return () => clearInterval(interval); - }, [isStreaming, effectiveApproval, parts]); - - const handleApprove = () => { - setUserApprovalOverride("user-approved"); - parts.forEach((part) => { - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: part.data.toolcallid, - approval: "user-approved", - }); - }); - }; - - const handleDeny = () => { - setUserApprovalOverride("user-denied"); - parts.forEach((part) => { - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: part.data.toolcallid, - approval: "user-denied", - }); - }); - }; - - return ( -
-
-
Reading Files
-
- {parts.map((part, idx) => ( - - ))} -
- {allNeedApproval && effectiveApproval === "needs-approval" && ( - - )} -
-
- ); -}); - -AIToolUseBatch.displayName = "AIToolUseBatch"; - -interface AIToolUseProps { - part: WaveUIMessagePart & { type: "data-tooluse" }; - isStreaming: boolean; -} - -const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { - const toolData = part.data; - const [userApprovalOverride, setUserApprovalOverride] = useState(null); - const highlightTimeoutRef = useRef(null); - const highlightedBlockIdRef = useRef(null); - - const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•"; - const statusColor = - toolData.status === "completed" ? "text-success" : toolData.status === "error" ? "text-error" : "text-gray-400"; - - const baseApproval = userApprovalOverride || toolData.approval; - const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; - - useEffect(() => { - if (!isStreaming || effectiveApproval !== "needs-approval") return; - - const interval = setInterval(() => { - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: toolData.toolcallid, - keepalive: true, - }); - }, 4000); - - return () => clearInterval(interval); - }, [isStreaming, effectiveApproval, toolData.toolcallid]); - - useEffect(() => { - return () => { - if (highlightTimeoutRef.current) { - clearTimeout(highlightTimeoutRef.current); - } - }; - }, []); - - const handleApprove = () => { - setUserApprovalOverride("user-approved"); - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: toolData.toolcallid, - approval: "user-approved", - }); - }; - - const handleDeny = () => { - setUserApprovalOverride("user-denied"); - RpcApi.WaveAIToolApproveCommand(TabRpcClient, { - toolcallid: toolData.toolcallid, - approval: "user-denied", - }); - }; - - const handleMouseEnter = () => { - if (!toolData.blockid) return; - - if (highlightTimeoutRef.current) { - clearTimeout(highlightTimeoutRef.current); - } - - highlightedBlockIdRef.current = toolData.blockid; - BlockModel.getInstance().setBlockHighlight({ - blockId: toolData.blockid, - icon: "sparkles", - }); - - highlightTimeoutRef.current = setTimeout(() => { - if (highlightedBlockIdRef.current === toolData.blockid) { - BlockModel.getInstance().setBlockHighlight(null); - highlightedBlockIdRef.current = null; - } - }, 2000); - }; - - const handleMouseLeave = () => { - if (!toolData.blockid) return; - - if (highlightTimeoutRef.current) { - clearTimeout(highlightTimeoutRef.current); - highlightTimeoutRef.current = null; - } - - if (highlightedBlockIdRef.current === toolData.blockid) { - BlockModel.getInstance().setBlockHighlight(null); - highlightedBlockIdRef.current = null; - } - }; - - return ( -
- {statusIcon} -
-
{toolData.toolname}
- {toolData.tooldesc &&
{toolData.tooldesc}
} - {(toolData.errormessage || effectiveApproval === "timeout") && ( -
{toolData.errormessage || "Not approved"}
- )} - {effectiveApproval === "needs-approval" && ( - - )} -
-
- ); -}); - -AIToolUse.displayName = "AIToolUse"; - -interface AIToolUseGroupProps { - parts: Array; - isStreaming: boolean; -} - -const AIToolUseGroup = memo(({ parts, isStreaming }: AIToolUseGroupProps) => { - const isFileOp = (part: WaveUIMessagePart & { type: "data-tooluse" }) => { - const toolName = part.data?.toolname; - return toolName === "read_text_file" || toolName === "read_dir"; - }; - - const fileOpsNeedApproval: Array = []; - const fileOpsNoApproval: Array = []; - const otherTools: Array = []; - - for (const part of parts) { - if (isFileOp(part)) { - if (part.data.approval === "needs-approval") { - fileOpsNeedApproval.push(part); - } else { - fileOpsNoApproval.push(part); - } - } else { - otherTools.push(part); - } - } - - return ( - <> - {fileOpsNoApproval.length > 0 && ( -
- -
- )} - {fileOpsNeedApproval.length > 0 && ( -
- -
- )} - {otherTools.map((tool, idx) => ( -
- -
- ))} - - ); -}); - -AIToolUseGroup.displayName = "AIToolUseGroup"; - interface AIMessagePartProps { part: WaveUIMessagePart; role: string; @@ -453,7 +168,11 @@ const groupMessageParts = (parts: WaveUIMessagePart[]): MessagePart[] => { return grouped; }; -const getThinkingMessage = (parts: WaveUIMessagePart[], isStreaming: boolean, role: string): { message: string; reasoningText?: string } | null => { +const getThinkingMessage = ( + parts: WaveUIMessagePart[], + isStreaming: boolean, + role: string +): { message: string; reasoningText?: string } | null => { if (!isStreaming || role !== "assistant") { return null; } @@ -520,6 +239,14 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => { )} {message.role === "user" && } + {message.role === "assistant" && !isStreaming && displayParts.length > 0 && ( + p.type === "text") + .map((p) => p.text || "") + .join("\n")} + /> + )}
); diff --git a/frontend/app/aipanel/aitooluse.tsx b/frontend/app/aipanel/aitooluse.tsx new file mode 100644 index 0000000000..171ce18562 --- /dev/null +++ b/frontend/app/aipanel/aitooluse.tsx @@ -0,0 +1,297 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { BlockModel } from "@/app/block/block-model"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; +import { cn } from "@/util/util"; +import { memo, useEffect, useRef, useState } from "react"; +import { WaveUIMessagePart } from "./aitypes"; + +interface AIToolApprovalButtonsProps { + count: number; + onApprove: () => void; + onDeny: () => void; +} + +const AIToolApprovalButtons = memo(({ count, onApprove, onDeny }: AIToolApprovalButtonsProps) => { + const approveText = count > 1 ? `Approve All (${count})` : "Approve"; + const denyText = count > 1 ? "Deny All" : "Deny"; + + return ( +
+ + +
+ ); +}); + +AIToolApprovalButtons.displayName = "AIToolApprovalButtons"; + +interface AIToolUseBatchItemProps { + part: WaveUIMessagePart & { type: "data-tooluse" }; + effectiveApproval: string; +} + +const AIToolUseBatchItem = memo(({ part, effectiveApproval }: AIToolUseBatchItemProps) => { + const statusIcon = part.data.status === "completed" ? "✓" : part.data.status === "error" ? "✗" : "•"; + const statusColor = + part.data.status === "completed" + ? "text-success" + : part.data.status === "error" + ? "text-error" + : "text-gray-400"; + const effectiveErrorMessage = part.data.errormessage || (effectiveApproval === "timeout" ? "Not approved" : null); + + return ( +
+ {statusIcon} +
+ {part.data.tooldesc} + {effectiveErrorMessage &&
{effectiveErrorMessage}
} +
+
+ ); +}); + +AIToolUseBatchItem.displayName = "AIToolUseBatchItem"; + +interface AIToolUseBatchProps { + parts: Array; + isStreaming: boolean; +} + +const AIToolUseBatch = memo(({ parts, isStreaming }: AIToolUseBatchProps) => { + const [userApprovalOverride, setUserApprovalOverride] = useState(null); + + const firstTool = parts[0].data; + const baseApproval = userApprovalOverride || firstTool.approval; + const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; + const allNeedApproval = parts.every((p) => (userApprovalOverride || p.data.approval) === "needs-approval"); + + useEffect(() => { + if (!isStreaming || effectiveApproval !== "needs-approval") return; + + const interval = setInterval(() => { + parts.forEach((part) => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + keepalive: true, + }); + }); + }, 4000); + + return () => clearInterval(interval); + }, [isStreaming, effectiveApproval, parts]); + + const handleApprove = () => { + setUserApprovalOverride("user-approved"); + parts.forEach((part) => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + approval: "user-approved", + }); + }); + }; + + const handleDeny = () => { + setUserApprovalOverride("user-denied"); + parts.forEach((part) => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: part.data.toolcallid, + approval: "user-denied", + }); + }); + }; + + return ( +
+
+
Reading Files
+
+ {parts.map((part, idx) => ( + + ))} +
+ {allNeedApproval && effectiveApproval === "needs-approval" && ( + + )} +
+
+ ); +}); + +AIToolUseBatch.displayName = "AIToolUseBatch"; + +interface AIToolUseProps { + part: WaveUIMessagePart & { type: "data-tooluse" }; + isStreaming: boolean; +} + +const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { + const toolData = part.data; + const [userApprovalOverride, setUserApprovalOverride] = useState(null); + const highlightTimeoutRef = useRef(null); + const highlightedBlockIdRef = useRef(null); + + const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•"; + const statusColor = + toolData.status === "completed" ? "text-success" : toolData.status === "error" ? "text-error" : "text-gray-400"; + + const baseApproval = userApprovalOverride || toolData.approval; + const effectiveApproval = !isStreaming && baseApproval === "needs-approval" ? "timeout" : baseApproval; + + useEffect(() => { + if (!isStreaming || effectiveApproval !== "needs-approval") return; + + const interval = setInterval(() => { + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: toolData.toolcallid, + keepalive: true, + }); + }, 4000); + + return () => clearInterval(interval); + }, [isStreaming, effectiveApproval, toolData.toolcallid]); + + useEffect(() => { + return () => { + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current); + } + }; + }, []); + + const handleApprove = () => { + setUserApprovalOverride("user-approved"); + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: toolData.toolcallid, + approval: "user-approved", + }); + }; + + const handleDeny = () => { + setUserApprovalOverride("user-denied"); + RpcApi.WaveAIToolApproveCommand(TabRpcClient, { + toolcallid: toolData.toolcallid, + approval: "user-denied", + }); + }; + + const handleMouseEnter = () => { + if (!toolData.blockid) return; + + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current); + } + + highlightedBlockIdRef.current = toolData.blockid; + BlockModel.getInstance().setBlockHighlight({ + blockId: toolData.blockid, + icon: "sparkles", + }); + + highlightTimeoutRef.current = setTimeout(() => { + if (highlightedBlockIdRef.current === toolData.blockid) { + BlockModel.getInstance().setBlockHighlight(null); + highlightedBlockIdRef.current = null; + } + }, 2000); + }; + + const handleMouseLeave = () => { + if (!toolData.blockid) return; + + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current); + highlightTimeoutRef.current = null; + } + + if (highlightedBlockIdRef.current === toolData.blockid) { + BlockModel.getInstance().setBlockHighlight(null); + highlightedBlockIdRef.current = null; + } + }; + + return ( +
+ {statusIcon} +
+
{toolData.toolname}
+ {toolData.tooldesc &&
{toolData.tooldesc}
} + {(toolData.errormessage || effectiveApproval === "timeout") && ( +
{toolData.errormessage || "Not approved"}
+ )} + {effectiveApproval === "needs-approval" && ( + + )} +
+
+ ); +}); + +AIToolUse.displayName = "AIToolUse"; + +interface AIToolUseGroupProps { + parts: Array; + isStreaming: boolean; +} + +export const AIToolUseGroup = memo(({ parts, isStreaming }: AIToolUseGroupProps) => { + const isFileOp = (part: WaveUIMessagePart & { type: "data-tooluse" }) => { + const toolName = part.data?.toolname; + return toolName === "read_text_file" || toolName === "read_dir"; + }; + + const fileOpsNeedApproval: Array = []; + const fileOpsNoApproval: Array = []; + const otherTools: Array = []; + + for (const part of parts) { + if (isFileOp(part)) { + if (part.data.approval === "needs-approval") { + fileOpsNeedApproval.push(part); + } else { + fileOpsNoApproval.push(part); + } + } else { + otherTools.push(part); + } + } + + return ( + <> + {fileOpsNoApproval.length > 0 && ( +
+ +
+ )} + {fileOpsNeedApproval.length > 0 && ( +
+ +
+ )} + {otherTools.map((tool, idx) => ( +
+ +
+ ))} + + ); +}); + +AIToolUseGroup.displayName = "AIToolUseGroup"; \ No newline at end of file diff --git a/frontend/app/element/emojibutton.tsx b/frontend/app/element/emojibutton.tsx index 00265ba070..069c82e8f5 100644 --- a/frontend/app/element/emojibutton.tsx +++ b/frontend/app/element/emojibutton.tsx @@ -1,20 +1,36 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { cn } from "@/util/util"; +import { cn, makeIconClass } from "@/util/util"; import { useLayoutEffect, useRef, useState } from "react"; -export const EmojiButton = ({ emoji, isClicked, onClick, className }: { emoji: string; isClicked: boolean; onClick: () => void; className?: string }) => { +export const EmojiButton = ({ + emoji, + icon, + isClicked, + onClick, + className, + suppressFlyUp, +}: { + emoji?: string; + icon?: string; + isClicked: boolean; + onClick: () => void; + className?: string; + suppressFlyUp?: boolean; +}) => { const [showFloating, setShowFloating] = useState(false); const prevClickedRef = useRef(isClicked); useLayoutEffect(() => { - if (isClicked && !prevClickedRef.current) { + if (isClicked && !prevClickedRef.current && !suppressFlyUp) { setShowFloating(true); setTimeout(() => setShowFloating(false), 600); } prevClickedRef.current = isClicked; - }, [isClicked]); + }, [isClicked, suppressFlyUp]); + + const content = icon ? : emoji; return (
@@ -28,7 +44,7 @@ export const EmojiButton = ({ emoji, isClicked, onClick, className }: { emoji: s className )} > - {emoji} + {content} {showFloating && ( - {emoji} + {content} )}
); -}; \ No newline at end of file +}; From 83452da70cbf4d4f99033277b545081247c18c4c Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 13:19:11 -0700 Subject: [PATCH 2/5] add feedback telemetry --- frontend/app/aipanel/aifeedbackbuttons.tsx | 24 +++++++++++++++++--- frontend/types/gotypes.d.ts | 1 + pkg/telemetry/telemetrydata/telemetrydata.go | 2 ++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/frontend/app/aipanel/aifeedbackbuttons.tsx b/frontend/app/aipanel/aifeedbackbuttons.tsx index c3b9f35539..83c8f3713e 100644 --- a/frontend/app/aipanel/aifeedbackbuttons.tsx +++ b/frontend/app/aipanel/aifeedbackbuttons.tsx @@ -1,6 +1,8 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; import { cn, makeIconClass } from "@/util/util"; import { memo, useState } from "react"; @@ -18,6 +20,14 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) if (thumbsDownClicked) { setThumbsDownClicked(false); } + if (!thumbsUpClicked) { + RpcApi.RecordTEventCommand(TabRpcClient, { + event: "waveai:feedback", + props: { + "waveai:feedback": "good", + }, + }); + } }; const handleThumbsDown = () => { @@ -25,6 +35,14 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) if (thumbsUpClicked) { setThumbsUpClicked(false); } + if (!thumbsDownClicked) { + RpcApi.RecordTEventCommand(TabRpcClient, { + event: "waveai:feedback", + props: { + "waveai:feedback": "bad", + }, + }); + } }; const handleCopy = () => { @@ -43,7 +61,7 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) ? "text-accent" : "text-secondary hover:bg-gray-700 hover:text-primary" )} - title="Good response" + title="Good Response" > @@ -55,7 +73,7 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) ? "text-accent" : "text-secondary hover:bg-gray-700 hover:text-primary" )} - title="Bad response" + title="Bad Response" > @@ -67,7 +85,7 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) ? "text-success" : "text-secondary hover:bg-gray-700 hover:text-primary" )} - title="Copy message" + title="Copy Message" > diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 0d2270bcd2..8a3e506450 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -994,6 +994,7 @@ declare global { "waveai:firstbytems"?: number; "waveai:requestdurms"?: number; "waveai:widgetaccess"?: boolean; + "waveai:feedback"?: "good" | "bad"; $set?: TEventUserProps; $set_once?: TEventUserProps; }; diff --git a/pkg/telemetry/telemetrydata/telemetrydata.go b/pkg/telemetry/telemetrydata/telemetrydata.go index 9a68d092af..f4984731d5 100644 --- a/pkg/telemetry/telemetrydata/telemetrydata.go +++ b/pkg/telemetry/telemetrydata/telemetrydata.go @@ -31,6 +31,7 @@ var ValidEventNames = map[string]bool{ "conn:connecterror": true, "waveai:enabletelemetry": true, "waveai:post": true, + "waveai:feedback": true, "onboarding:start": true, "onboarding:skip": true, "onboarding:fire": true, @@ -131,6 +132,7 @@ type TEventProps struct { WaveAIFirstByteMs int `json:"waveai:firstbytems,omitempty"` // ms WaveAIRequestDurMs int `json:"waveai:requestdurms,omitempty"` // ms WaveAIWidgetAccess bool `json:"waveai:widgetaccess,omitempty"` + WaveAIFeedback string `json:"waveai:feedback,omitempty" tstype:"\"good\" | \"bad\""` UserSet *TEventUserProps `json:"$set,omitempty"` UserSetOnce *TEventUserProps `json:"$set_once,omitempty"` From 5ecc7c2ae7b9b9efa8b6651580892588da2a0235 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 13:30:58 -0700 Subject: [PATCH 3/5] update this message to avoid saying what the cap is explicitly --- frontend/app/aipanel/aipanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index aca6f17f93..6a91d48b7b 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -156,7 +156,7 @@ const AIWelcomeMessage = memo(() => {
- (BETA: 50 free requests daily) + BETA: Free to use. Daily limits keep our costs in check.
From bc30c5de15e80ddf4f965236cc2bf498c21c477d Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 14:08:24 -0700 Subject: [PATCH 4/5] minor, don't show copy button if no text. use \n\n as separator for text parts. --- frontend/app/aipanel/aifeedbackbuttons.tsx | 26 ++++++++++++---------- frontend/app/aipanel/aimessage.tsx | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/app/aipanel/aifeedbackbuttons.tsx b/frontend/app/aipanel/aifeedbackbuttons.tsx index 83c8f3713e..916a4cdc89 100644 --- a/frontend/app/aipanel/aifeedbackbuttons.tsx +++ b/frontend/app/aipanel/aifeedbackbuttons.tsx @@ -77,18 +77,20 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) > - + {messageText?.trim() && ( + + )} ); }); diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index 7c187d1ae9..7a4eecfdd0 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -244,7 +244,7 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => { messageText={parts .filter((p) => p.type === "text") .map((p) => p.text || "") - .join("\n")} + .join("\n\n")} /> )} From f87e68c37d4c371c281b9c8a742e4637ae927dc4 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 14:18:09 -0700 Subject: [PATCH 5/5] small tweak to change "//" => "/" for OSC 7 --- frontend/app/view/term/termwrap.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 6409a86bd7..7f623cf39b 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -131,6 +131,11 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool } pathPart = decodeURIComponent(url.pathname); + // Normalize double slashes at the beginning to single slash + if (pathPart.startsWith("//")) { + pathPart = pathPart.substring(1); + } + // Handle Windows paths (e.g., /C:/... or /D:\...) if (/^\/[a-zA-Z]:[\\/]/.test(pathPart)) { // Strip leading slash and normalize to forward slashes