Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 90 additions & 7 deletions frontend/app/aipanel/aimessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

import { WaveStreamdown } from "@/app/element/streamdown";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { cn } from "@/util/util";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { memo, useEffect, useState } from "react";
import { getFileIcon } from "./ai-utils";
import { WaveUIMessage, WaveUIMessagePart } from "./aitypes";
import { WaveAIModel } from "./waveai-model";
Expand Down Expand Up @@ -67,6 +68,84 @@ const UserMessageFiles = memo(({ fileParts }: UserMessageFilesProps) => {

UserMessageFiles.displayName = "UserMessageFiles";

interface AIToolUseProps {
part: WaveUIMessagePart & { type: "data-tooluse" };
isStreaming: boolean;
}

const AIToolUse = memo(({ part }: AIToolUseProps) => {
const toolData = part.data;
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);

const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
const statusColor =
toolData.status === "completed"
? "text-green-400"
: toolData.status === "error"
? "text-red-400"
: "text-gray-400";

const effectiveApproval = userApprovalOverride || toolData.approval;

useEffect(() => {
if (effectiveApproval !== "needs-approval") return;

const interval = setInterval(() => {
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
toolcallid: toolData.toolcallid,
keepalive: true,
});
}, 4000);

return () => clearInterval(interval);
}, [effectiveApproval, toolData.toolcallid]);

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",
});
};

return (
<div className={cn("flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700", statusColor)}>
<span className="font-bold">{statusIcon}</span>
<div className="flex-1">
<div className="font-semibold">{toolData.toolname}</div>
{toolData.tooldesc && <div className="text-sm text-gray-400">{toolData.tooldesc}</div>}
{toolData.errormessage && <div className="text-sm text-red-300 mt-1">{toolData.errormessage}</div>}
{effectiveApproval === "needs-approval" && (
<div className="mt-2 flex gap-2">
<button
onClick={handleApprove}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Approve
</button>
<button
onClick={handleDeny}
className="px-3 py-1 border border-gray-600 text-gray-300 hover:border-gray-500 hover:text-white text-sm rounded cursor-pointer transition-colors"
>
Deny
</button>
</div>
)}
</div>
</div>
);
});

AIToolUse.displayName = "AIToolUse";

interface AIMessagePartProps {
part: WaveUIMessagePart;
role: string;
Expand All @@ -93,9 +172,8 @@ const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) =>
}
}

if (part.type.startsWith("tool-") && "state" in part && part.state === "input-available") {
const toolName = part.type.substring(5); // Remove "tool-" prefix
return <div className="text-gray-400 italic">Calling tool {toolName}</div>;
if (part.type === "data-tooluse" && part.data) {
return <AIToolUse part={part as WaveUIMessagePart & { type: "data-tooluse" }} isStreaming={isStreaming} />;
}

return null;
Expand All @@ -110,7 +188,9 @@ interface AIMessageProps {

const isDisplayPart = (part: WaveUIMessagePart): boolean => {
return (
part.type === "text" || (part.type.startsWith("tool-") && "state" in part && part.state === "input-available")
part.type === "text" ||
part.type === "data-tooluse" ||
(part.type.startsWith("tool-") && "state" in part && part.state === "input-available")
);
};

Expand All @@ -122,7 +202,10 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
);
const hasContent =
displayParts.length > 0 &&
displayParts.some((part) => (part.type === "text" && part.text) || part.type.startsWith("tool-"));
displayParts.some(
(part) =>
(part.type === "text" && part.text) || part.type.startsWith("tool-") || part.type === "data-tooluse"
);

const showThinkingOnly = !hasContent && isStreaming && message.role === "assistant";
const showThinkingInline = hasContent && isStreaming && message.role === "assistant";
Expand Down
8 changes: 8 additions & 0 deletions frontend/app/aipanel/aitypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ type WaveUIDataTypes = {
mimetype: string;
previewurl?: string;
};
tooluse: {
toolcallid: string;
toolname: string;
tooldesc: string;
status: "pending" | "error" | "completed";
errormessage?: string;
approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout";
};
};

export type WaveUIMessage = UIMessage<unknown, WaveUIDataTypes, {}>;
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,11 @@ class RpcApiType {
return client.wshRpcCall("waveaienabletelemetry", null, opts);
}

// command "waveaitoolapprove" [call]
WaveAIToolApproveCommand(client: WshClient, data: CommandWaveAIToolApproveData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("waveaitoolapprove", data, opts);
}

// command "waveinfo" [call]
WaveInfoCommand(client: WshClient, opts?: RpcOpts): Promise<WaveInfoData> {
return client.wshRpcCall("waveinfo", null, opts);
Expand Down
6 changes: 3 additions & 3 deletions frontend/app/tab/tabbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { modalsModel } from "@/app/store/modalmodel";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { WindowDrag } from "@/element/windowdrag";
import { deleteLayoutModelForTab } from "@/layout/index";
import { atoms, createTab, getApi, globalStore, isDev, setActiveTab } from "@/store/global";
import { atoms, createTab, getApi, globalStore, setActiveTab } from "@/store/global";
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
import { fireAndForget } from "@/util/util";
import { useAtomValue } from "jotai";
Expand Down Expand Up @@ -640,7 +640,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
}

const tabsWrapperWidth = tabIds.length * tabWidthRef.current;
const waveaiButton = isDev() ? (
const waveaiButton = (
<div
className={`flex h-[26px] px-1.5 justify-end items-center rounded-md mr-1 box-border cursor-pointer bg-hover hover:bg-hoverbg transition-colors text-[12px] ${aiPanelOpen ? "text-accent" : "text-secondary"}`}
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
Expand All @@ -649,7 +649,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
<i className="fa fa-sparkles" />
<span className="font-bold ml-1 -top-px font-mono">AI</span>
</div>
) : undefined;
);
const appMenuButton =
PLATFORM !== PlatformMacOS && !settings["window:showmenubar"] ? (
<div ref={appMenuButtonRef} className="app-menu-button" onClick={onEllipsisClick}>
Expand Down
10 changes: 2 additions & 8 deletions frontend/app/workspace/workspace-layout-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as WOS from "@/app/store/wos";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
import { atoms, getApi, getTabMetaKeyAtom, isDev, recordTEvent, refocusNode } from "@/store/global";
import { atoms, getApi, getTabMetaKeyAtom, recordTEvent, refocusNode } from "@/store/global";
import debug from "debug";
import * as jotai from "jotai";
import { debounce } from "lodash-es";
Expand Down Expand Up @@ -42,7 +42,7 @@ class WorkspaceLayoutModel {
this.panelContainerRef = null;
this.aiPanelWrapperRef = null;
this.inResize = false;
this.aiPanelVisible = isDev();
this.aiPanelVisible = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the AI panel is now generally available and ready for production.

The AI panel now defaults to visible for all users (previously gated by isDev()). This is a significant UX change that exposes the new tool-use features to production.

Please confirm:

  1. This change is intentional and aligns with the product roadmap for making AI features generally available.
  2. All isDev() checks related to AI panel functionality have been removed consistently across the codebase (frontend components, handlers, etc.).
  3. The AI panel and tool-use features are fully functional and tested in production environments.
  4. Existing users who may have had implicit "closed" state won't experience issues (the code shows visibility is persisted in tab metadata at lines 76-82, which should handle this).

Run the following script to verify that all isDev() guards related to AI panel functionality have been removed:


🏁 Script executed:

#!/bin/bash
# Description: Search for remaining isDev() checks related to AI panel or WaveAI functionality.

# Search for isDev usage in frontend AI-related files
echo "=== Searching for isDev() in AI-related frontend files ==="
rg -n --type=ts -C3 'isDev\s*\(' frontend/app/aipanel/ frontend/app/workspace/

# Search for isDev usage specifically around aiPanel or waveai keywords
echo -e "\n=== Searching for isDev() near AI panel keywords ==="
rg -nP --type=ts -C5 'isDev\s*\(' | rg -C5 '(?i)(aipanel|waveai|ai.*panel)'

# Search for any commented-out isDev checks that might indicate incomplete removal
echo -e "\n=== Searching for commented isDev() references ==="
rg -n --type=ts '//.*isDev' frontend/app/aipanel/ frontend/app/workspace/

Length of output: 2992


Remove leftover isDev() guards on AI features

The following dev‐only conditionals remain and must be removed so AI features are always available:

  • frontend/app/workspace/widgets.tsx (Wave Dev Build label at lines ~222–225 & 246–249)
  • frontend/app/tab/tabbar.tsx (waveaiButton at lines ~643–648)

Remove these isDev() checks to ensure consistent production visibility.

this.aiPanelWidth = null;
this.panelVisibleAtom = jotai.atom(this.aiPanelVisible);

Expand Down Expand Up @@ -219,9 +219,6 @@ class WorkspaceLayoutModel {
}

setAIPanelVisible(visible: boolean): void {
if (!isDev() && visible) {
return;
}
if (this.focusTimeoutRef != null) {
clearTimeout(this.focusTimeoutRef);
this.focusTimeoutRef = null;
Expand Down Expand Up @@ -290,9 +287,6 @@ class WorkspaceLayoutModel {
}

handleAIPanelResize(width: number, windowWidth: number): void {
if (!isDev()) {
return;
}
if (!this.getAIPanelVisible()) {
return;
}
Expand Down
8 changes: 8 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ declare global {
waitms: number;
};

// wshrpc.CommandWaveAIToolApproveData
type CommandWaveAIToolApproveData = {
toolcallid: string;
keepalive?: boolean;
approval?: string;
};

// wshrpc.CommandWebSelectorData
type CommandWebSelectorData = {
workspaceid: string;
Expand Down Expand Up @@ -944,6 +951,7 @@ declare global {
"waveai:outputtokens"?: number;
"waveai:requestcount"?: number;
"waveai:toolusecount"?: number;
"waveai:tooluseerrorcount"?: number;
"waveai:tooldetail"?: {[key: string]: number};
"waveai:premiumreq"?: number;
"waveai:proxyreq"?: number;
Expand Down
Loading
Loading