diff --git a/frontend/app/aipanel/aifeedbackbuttons.tsx b/frontend/app/aipanel/aifeedbackbuttons.tsx
new file mode 100644
index 0000000000..916a4cdc89
--- /dev/null
+++ b/frontend/app/aipanel/aifeedbackbuttons.tsx
@@ -0,0 +1,98 @@
+// 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";
+
+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);
+ }
+ if (!thumbsUpClicked) {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "waveai:feedback",
+ props: {
+ "waveai:feedback": "good",
+ },
+ });
+ }
+ };
+
+ const handleThumbsDown = () => {
+ setThumbsDownClicked(!thumbsDownClicked);
+ if (thumbsUpClicked) {
+ setThumbsUpClicked(false);
+ }
+ if (!thumbsDownClicked) {
+ RpcApi.RecordTEventCommand(TabRpcClient, {
+ event: "waveai:feedback",
+ props: {
+ "waveai:feedback": "bad",
+ },
+ });
+ }
+ };
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(messageText);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+
+ {messageText?.trim() && (
+
+ )}
+
+ );
+});
+
+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..7a4eecfdd0 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\n")}
+ />
+ )}
);
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.
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
+};
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
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"`