Skip to content

Commit fd0e75a

Browse files
authored
New data-tooluse AI SDK packet and Tool Approvals Implemented (#2407)
provides richer information for FE to use to display tools. also implements a full approve/deny flow for tools that require approval (readfile)
1 parent 7681214 commit fd0e75a

File tree

20 files changed

+754
-192
lines changed

20 files changed

+754
-192
lines changed

frontend/app/aipanel/aimessage.tsx

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// SPDX-License-Identifier: Apache-2.0
33

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

6869
UserMessageFiles.displayName = "UserMessageFiles";
6970

71+
interface AIToolUseProps {
72+
part: WaveUIMessagePart & { type: "data-tooluse" };
73+
isStreaming: boolean;
74+
}
75+
76+
const AIToolUse = memo(({ part }: AIToolUseProps) => {
77+
const toolData = part.data;
78+
const [userApprovalOverride, setUserApprovalOverride] = useState<string | null>(null);
79+
80+
const statusIcon = toolData.status === "completed" ? "✓" : toolData.status === "error" ? "✗" : "•";
81+
const statusColor =
82+
toolData.status === "completed"
83+
? "text-green-400"
84+
: toolData.status === "error"
85+
? "text-red-400"
86+
: "text-gray-400";
87+
88+
const effectiveApproval = userApprovalOverride || toolData.approval;
89+
90+
useEffect(() => {
91+
if (effectiveApproval !== "needs-approval") return;
92+
93+
const interval = setInterval(() => {
94+
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
95+
toolcallid: toolData.toolcallid,
96+
keepalive: true,
97+
});
98+
}, 4000);
99+
100+
return () => clearInterval(interval);
101+
}, [effectiveApproval, toolData.toolcallid]);
102+
103+
const handleApprove = () => {
104+
setUserApprovalOverride("user-approved");
105+
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
106+
toolcallid: toolData.toolcallid,
107+
approval: "user-approved",
108+
});
109+
};
110+
111+
const handleDeny = () => {
112+
setUserApprovalOverride("user-denied");
113+
RpcApi.WaveAIToolApproveCommand(TabRpcClient, {
114+
toolcallid: toolData.toolcallid,
115+
approval: "user-denied",
116+
});
117+
};
118+
119+
return (
120+
<div className={cn("flex items-start gap-2 p-2 rounded bg-gray-800 border border-gray-700", statusColor)}>
121+
<span className="font-bold">{statusIcon}</span>
122+
<div className="flex-1">
123+
<div className="font-semibold">{toolData.toolname}</div>
124+
{toolData.tooldesc && <div className="text-sm text-gray-400">{toolData.tooldesc}</div>}
125+
{toolData.errormessage && <div className="text-sm text-red-300 mt-1">{toolData.errormessage}</div>}
126+
{effectiveApproval === "needs-approval" && (
127+
<div className="mt-2 flex gap-2">
128+
<button
129+
onClick={handleApprove}
130+
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"
131+
>
132+
Approve
133+
</button>
134+
<button
135+
onClick={handleDeny}
136+
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"
137+
>
138+
Deny
139+
</button>
140+
</div>
141+
)}
142+
</div>
143+
</div>
144+
);
145+
});
146+
147+
AIToolUse.displayName = "AIToolUse";
148+
70149
interface AIMessagePartProps {
71150
part: WaveUIMessagePart;
72151
role: string;
@@ -93,9 +172,8 @@ const AIMessagePart = memo(({ part, role, isStreaming }: AIMessagePartProps) =>
93172
}
94173
}
95174

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

101179
return null;
@@ -110,7 +188,9 @@ interface AIMessageProps {
110188

111189
const isDisplayPart = (part: WaveUIMessagePart): boolean => {
112190
return (
113-
part.type === "text" || (part.type.startsWith("tool-") && "state" in part && part.state === "input-available")
191+
part.type === "text" ||
192+
part.type === "data-tooluse" ||
193+
(part.type.startsWith("tool-") && "state" in part && part.state === "input-available")
114194
);
115195
};
116196

@@ -122,7 +202,10 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
122202
);
123203
const hasContent =
124204
displayParts.length > 0 &&
125-
displayParts.some((part) => (part.type === "text" && part.text) || part.type.startsWith("tool-"));
205+
displayParts.some(
206+
(part) =>
207+
(part.type === "text" && part.text) || part.type.startsWith("tool-") || part.type === "data-tooluse"
208+
);
126209

127210
const showThinkingOnly = !hasContent && isStreaming && message.role === "assistant";
128211
const showThinkingInline = hasContent && isStreaming && message.role === "assistant";

frontend/app/aipanel/aitypes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ type WaveUIDataTypes = {
1010
mimetype: string;
1111
previewurl?: string;
1212
};
13+
tooluse: {
14+
toolcallid: string;
15+
toolname: string;
16+
tooldesc: string;
17+
status: "pending" | "error" | "completed";
18+
errormessage?: string;
19+
approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout";
20+
};
1321
};
1422

1523
export type WaveUIMessage = UIMessage<unknown, WaveUIDataTypes, {}>;

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ class RpcApiType {
492492
return client.wshRpcCall("waveaienabletelemetry", null, opts);
493493
}
494494

495+
// command "waveaitoolapprove" [call]
496+
WaveAIToolApproveCommand(client: WshClient, data: CommandWaveAIToolApproveData, opts?: RpcOpts): Promise<void> {
497+
return client.wshRpcCall("waveaitoolapprove", data, opts);
498+
}
499+
495500
// command "waveinfo" [call]
496501
WaveInfoCommand(client: WshClient, opts?: RpcOpts): Promise<WaveInfoData> {
497502
return client.wshRpcCall("waveinfo", null, opts);

frontend/app/tab/tabbar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { modalsModel } from "@/app/store/modalmodel";
66
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
77
import { WindowDrag } from "@/element/windowdrag";
88
import { deleteLayoutModelForTab } from "@/layout/index";
9-
import { atoms, createTab, getApi, globalStore, isDev, setActiveTab } from "@/store/global";
9+
import { atoms, createTab, getApi, globalStore, setActiveTab } from "@/store/global";
1010
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
1111
import { fireAndForget } from "@/util/util";
1212
import { useAtomValue } from "jotai";
@@ -640,7 +640,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
640640
}
641641

642642
const tabsWrapperWidth = tabIds.length * tabWidthRef.current;
643-
const waveaiButton = isDev() ? (
643+
const waveaiButton = (
644644
<div
645645
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"}`}
646646
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
@@ -649,7 +649,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
649649
<i className="fa fa-sparkles" />
650650
<span className="font-bold ml-1 -top-px font-mono">AI</span>
651651
</div>
652-
) : undefined;
652+
);
653653
const appMenuButton =
654654
PLATFORM !== PlatformMacOS && !settings["window:showmenubar"] ? (
655655
<div ref={appMenuButtonRef} className="app-menu-button" onClick={onEllipsisClick}>

frontend/app/workspace/workspace-layout-model.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as WOS from "@/app/store/wos";
77
import { RpcApi } from "@/app/store/wshclientapi";
88
import { TabRpcClient } from "@/app/store/wshrpcutil";
99
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
10-
import { atoms, getApi, getTabMetaKeyAtom, isDev, recordTEvent, refocusNode } from "@/store/global";
10+
import { atoms, getApi, getTabMetaKeyAtom, recordTEvent, refocusNode } from "@/store/global";
1111
import debug from "debug";
1212
import * as jotai from "jotai";
1313
import { debounce } from "lodash-es";
@@ -42,7 +42,7 @@ class WorkspaceLayoutModel {
4242
this.panelContainerRef = null;
4343
this.aiPanelWrapperRef = null;
4444
this.inResize = false;
45-
this.aiPanelVisible = isDev();
45+
this.aiPanelVisible = true;
4646
this.aiPanelWidth = null;
4747
this.panelVisibleAtom = jotai.atom(this.aiPanelVisible);
4848

@@ -219,9 +219,6 @@ class WorkspaceLayoutModel {
219219
}
220220

221221
setAIPanelVisible(visible: boolean): void {
222-
if (!isDev() && visible) {
223-
return;
224-
}
225222
if (this.focusTimeoutRef != null) {
226223
clearTimeout(this.focusTimeoutRef);
227224
this.focusTimeoutRef = null;
@@ -290,9 +287,6 @@ class WorkspaceLayoutModel {
290287
}
291288

292289
handleAIPanelResize(width: number, windowWidth: number): void {
293-
if (!isDev()) {
294-
return;
295-
}
296290
if (!this.getAIPanelVisible()) {
297291
return;
298292
}

frontend/types/gotypes.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,13 @@ declare global {
320320
waitms: number;
321321
};
322322

323+
// wshrpc.CommandWaveAIToolApproveData
324+
type CommandWaveAIToolApproveData = {
325+
toolcallid: string;
326+
keepalive?: boolean;
327+
approval?: string;
328+
};
329+
323330
// wshrpc.CommandWebSelectorData
324331
type CommandWebSelectorData = {
325332
workspaceid: string;
@@ -944,6 +951,7 @@ declare global {
944951
"waveai:outputtokens"?: number;
945952
"waveai:requestcount"?: number;
946953
"waveai:toolusecount"?: number;
954+
"waveai:tooluseerrorcount"?: number;
947955
"waveai:tooldetail"?: {[key: string]: number};
948956
"waveai:premiumreq"?: number;
949957
"waveai:proxyreq"?: number;

0 commit comments

Comments
 (0)