diff --git a/docs/docs/ai-presets.mdx b/docs/docs/ai-presets.mdx index 7af440692b..b8c7b34546 100644 --- a/docs/docs/ai-presets.mdx +++ b/docs/docs/ai-presets.mdx @@ -3,6 +3,10 @@ sidebar_position: 3.6 id: "ai-presets" title: "AI Presets" --- +:::warning Deprecation Notice +The AI Widget and its presets are being replaced by [Wave AI](./waveai.mdx). Please refer to the Wave AI documentation for the latest AI features and configuration options. +::: + ![AI Presets Menu](./img/ai-presets.png#right) diff --git a/docs/docs/customization.mdx b/docs/docs/customization.mdx index ed290d0a68..02fedca70a 100644 --- a/docs/docs/customization.mdx +++ b/docs/docs/customization.mdx @@ -81,6 +81,4 @@ wsh setbg --print "#ff0000" For more advanced customization options including gradients, colors, and saving your own background presets, check out our [Background Configuration](/presets#background-configurations) documentation. -## Presets -For more advanced customization, to set up multiple AI models, and your own tab backgrounds, check out our [Presets Documentation](./presets). diff --git a/docs/docs/faq.mdx b/docs/docs/faq.mdx index 559944c4e4..74967cbb91 100644 --- a/docs/docs/faq.mdx +++ b/docs/docs/faq.mdx @@ -6,12 +6,6 @@ title: "FAQ" # FAQ -### How do I configure Wave to use different AI models/providers? - -Wave supports various AI providers including local LLMs (via Ollama), Azure OpenAI, Anthropic's Claude, and Perplexity. The recommended way to configure these is through AI presets, which let you set up and easily switch between different providers and models. - -See our [AI Presets documentation](/ai-presets) for detailed setup instructions for each provider. - ### How do I enable Claude Code support with Shift+Enter? Wave supports Claude Code and similar AI coding tools that expect Shift+Enter to send an escape sequence + newline (`\u001b\n`) instead of a regular carriage return. This can be enabled using the `term:shiftenternewline` configuration setting. diff --git a/docs/docs/gettingstarted.mdx b/docs/docs/gettingstarted.mdx index cffcdbf083..e9df15ca54 100644 --- a/docs/docs/gettingstarted.mdx +++ b/docs/docs/gettingstarted.mdx @@ -90,7 +90,7 @@ You can also download installers directly from our [Downloads page](https://www. ### Tabs and Blocks - **Tabs**: Like browser tabs, these help organize your work. Create new tabs with . -- **Blocks**: The building blocks of Wave. Each block can be a terminal, web browser, file preview, AI chat, or other widget. +- **Blocks**: The building blocks of Wave. Each block can be a terminal, web browser, file preview, or other widget. - **Layout**: Blocks can be dragged, dropped, and resized to create your ideal layout. ### Key Features @@ -152,7 +152,7 @@ You can also download installers directly from our [Downloads page](https://www. - Explore [Key Bindings](./keybindings) to work more efficiently - Learn about [Tab Layouts](./layout) to organize your workspace - Set up [Custom Widgets](./customwidgets) for quick access to your tools -- Configure [AI Presets](./ai-presets) to use your preferred AI models +- Configure [Wave AI](./waveai) to use your preferred AI models - Check out [Configuration](./config) for detailed customization options ## Getting Help diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index 1c5ee7f3f8..f1665faae8 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -19,6 +19,12 @@ Check out [Getting Started](./gettingstarted) for installation instructions. ![Wave Screenshot](./img/wave-screenshot.webp) + | Clear AI Chat | +| Key | Function | +| ----------------------- | ----------------------- | +| | Toggle WaveAI panel | +| | Focus WaveAI input | +| | Clear AI Chat | ## Terminal Keybindings diff --git a/docs/docs/presets.mdx b/docs/docs/presets.mdx index 158f35c7a7..31fc5f57d7 100644 --- a/docs/docs/presets.mdx +++ b/docs/docs/presets.mdx @@ -6,9 +6,8 @@ title: "Presets" # Presets -Wave's preset system allows you to save and apply multiple configuration settings at once. Presets can be used in two different scenarios: +Wave's preset system allows you to save and apply multiple configuration settings at once. Presets are used for: -- AI models: Configure different AI providers and models (see [AI Presets](/ai-presets)) - Tab backgrounds: Apply visual styles to your tabs ## Managing Presets @@ -18,14 +17,14 @@ You can store presets in two locations: - `~/.config/waveterm/presets.json`: Main presets file - `~/.config/waveterm/presets/`: Directory for organizing presets into separate files -All presets are aggregated regardless of which file they're in, so you can use the `presets` directory to organize them (e.g., `presets/bg.json`, `presets/ai.json`). +All presets are aggregated regardless of which file they're in, so you can use the `presets` directory to organize them (e.g., `presets/bg.json`). :::info You can easily edit your presets using the built-in editor: ```bash wsh editconfig presets.json # Edit main presets file -wsh editconfig presets/ai.json # Edit AI presets +wsh editconfig presets/bg.json # Edit background presets ``` ::: @@ -47,7 +46,6 @@ Presets follow this format: The `preset-type` determines where the preset appears in Wave's interface: -- `ai`: Appears in the models dropdown in the "Wave AI" widget header (see [AI Presets](/ai-presets)) - `bg`: Appears in the "Backgrounds" submenu when right-clicking a tab ### Common Keys @@ -58,19 +56,9 @@ The `preset-type` determines where the preset appears in Wave's interface: | display:order | float | Controls the order in the menu (optional) | :::info -When a preset is applied, it overrides the default configuration values for that tab or block. Using `bg:*` or `ai:*` will clear any previously overridden values, setting them back to defaults. It's recommended to include these keys in your presets to ensure a clean slate. +When a preset is applied, it overrides the default configuration values for that tab or block. Using `bg:*` will clear any previously overridden values, setting them back to defaults. It's recommended to include this key in your presets to ensure a clean slate. ::: -## AI Presets - -For configuring AI providers and models, see our dedicated [AI Presets](/ai-presets) documentation. It covers setting up presets for: - -- Local LLMs via Ollama -- Azure OpenAI -- Anthropic's Claude -- Perplexity -- And more - ## Background Presets Wave's background system harnesses the full power of CSS backgrounds, letting you create rich visual effects through the "background" attribute. You can apply solid colors, gradients (both linear and radial), images, and even blend multiple elements together. diff --git a/frontend/app/aipanel/aimessage.tsx b/frontend/app/aipanel/aimessage.tsx index 02ccb5d947..e426e9114a 100644 --- a/frontend/app/aipanel/aimessage.tsx +++ b/frontend/app/aipanel/aimessage.tsx @@ -1,11 +1,12 @@ // 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, useState } from "react"; +import { memo, useEffect, useRef, useState } from "react"; import { getFileIcon } from "./ai-utils"; import { WaveUIMessage, WaveUIMessagePart } from "./aitypes"; import { WaveAIModel } from "./waveai-model"; @@ -201,6 +202,8 @@ interface AIToolUseProps { 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 = @@ -222,6 +225,14 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { return () => clearInterval(interval); }, [isStreaming, effectiveApproval, toolData.toolcallid]); + useEffect(() => { + return () => { + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current); + } + }; + }, []); + const handleApprove = () => { setUserApprovalOverride("user-approved"); RpcApi.WaveAIToolApproveCommand(TabRpcClient, { @@ -238,8 +249,47 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => { }); }; + 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}
diff --git a/frontend/app/aipanel/aitypes.ts b/frontend/app/aipanel/aitypes.ts index f16c1f6e3c..98bbc0756c 100644 --- a/frontend/app/aipanel/aitypes.ts +++ b/frontend/app/aipanel/aitypes.ts @@ -17,6 +17,7 @@ type WaveUIDataTypes = { status: "pending" | "error" | "completed"; errormessage?: string; approval?: "needs-approval" | "user-approved" | "user-denied" | "auto-approved" | "timeout"; + blockid?: string; }; }; diff --git a/frontend/app/block/block-model.ts b/frontend/app/block/block-model.ts new file mode 100644 index 0000000000..e2ce23e374 --- /dev/null +++ b/frontend/app/block/block-model.ts @@ -0,0 +1,51 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { globalStore } from "@/app/store/jotaiStore"; +import * as jotai from "jotai"; + +export interface BlockHighlightType { + blockId: string; + icon: string; +} + +export class BlockModel { + private static instance: BlockModel | null = null; + private blockHighlightAtomCache = new Map>(); + + blockHighlightAtom: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; + + private constructor() { + // Empty for now + } + + getBlockHighlightAtom(blockId: string): jotai.Atom { + let atom = this.blockHighlightAtomCache.get(blockId); + if (!atom) { + atom = jotai.atom((get) => { + const highlight = get(this.blockHighlightAtom); + if (highlight?.blockId === blockId) { + return highlight; + } + return null; + }); + this.blockHighlightAtomCache.set(blockId, atom); + } + return atom; + } + + setBlockHighlight(highlight: BlockHighlightType | null) { + globalStore.set(this.blockHighlightAtom, highlight); + } + + static getInstance(): BlockModel { + if (!BlockModel.instance) { + BlockModel.instance = new BlockModel(); + } + return BlockModel.instance; + } + + static resetInstance(): void { + BlockModel.instance = null; + } +} \ No newline at end of file diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 7e788b5cfd..7920c5e8b3 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -1,6 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +import { BlockModel } from "@/app/block/block-model"; import { blockViewToIcon, blockViewToName, ConnectionButton, getBlockHeaderIcon, Input } from "@/app/block/blockutil"; import { Button } from "@/app/element/button"; import { useDimensionsWithCallbackRef } from "@/app/hook/useDimensions"; @@ -26,6 +27,7 @@ import { MagnifyIcon } from "@/element/magnify"; import { MenuButton } from "@/element/menubutton"; import { NodeModel } from "@/layout/index"; import * as util from "@/util/util"; +import { makeIconClass } from "@/util/util"; import { computeBgStyleFromMeta } from "@/util/waveutil"; import clsx from "clsx"; import * as jotai from "jotai"; @@ -483,9 +485,11 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const blockNum = jotai.useAtomValue(nodeModel.blockNum); const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true; + const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); const [blockData] = WOS.useWaveObjectValue(WOS.makeORef("block", nodeModel.blockId)); const style: React.CSSProperties = {}; let showBlockMask = false; + if (isFocused) { const tabData = jotai.useAtomValue(atoms.tabAtom); const tabActiveBorderColor = tabData?.meta?.["bg:activebordercolor"]; @@ -505,6 +509,11 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { style.borderColor = blockData.meta["frame:bordercolor"]; } } + + if (blockHighlight && !style.borderColor) { + style.borderColor = "rgb(59, 130, 246)"; + } + let innerElem = null; if (isLayoutMode && showOverlayBlockNums) { showBlockMask = true; @@ -513,9 +522,18 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => {
{blockNum}
); + } else if (blockHighlight) { + showBlockMask = true; + const iconClass = makeIconClass(blockHighlight.icon, false); + innerElem = ( +
+ +
+ ); } + return ( -
+
{innerElem}
); diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 425f8ee7d3..9a1b273143 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -104,6 +104,18 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const settingsAtom = atom((get) => { return get(fullConfigAtom)?.settings ?? {}; }) as Atom; + const hasCustomAIPresetsAtom = atom((get) => { + const fullConfig = get(fullConfigAtom); + if (!fullConfig?.presets) { + return false; + } + for (const presetId in fullConfig.presets) { + if (presetId.startsWith("ai@") && presetId !== "ai@global" && presetId !== "ai@wave") { + return true; + } + } + return false; + }) as Atom; const tabAtom: Atom = atom((get) => { return WOS.getObjectValue(WOS.makeORef("tab", initOpts.tabId), get); }); @@ -160,6 +172,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { workspace: workspaceAtom, fullConfigAtom, settingsAtom, + hasCustomAIPresetsAtom, tabAtom, staticTabId: staticTabIdAtom, isFullScreen: isFullScreenAtom, diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index b56471c4d5..f22e784cf6 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -69,6 +69,7 @@ const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal const Widgets = memo(() => { const fullConfig = useAtomValue(atoms.fullConfigAtom); + const hasCustomAIPresets = useAtomValue(atoms.hasCustomAIPresetsAtom); const [mode, setMode] = useState<"normal" | "compact" | "supercompact">("normal"); const containerRef = useRef(null); const measurementRef = useRef(null); @@ -93,7 +94,11 @@ const Widgets = memo(() => { magnified: true, }; const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true; - const widgets = sortByDisplayOrder(fullConfig?.widgets); + const widgetsMap = fullConfig?.widgets ?? {}; + const filteredWidgets = hasCustomAIPresets + ? widgetsMap + : Object.fromEntries(Object.entries(widgetsMap).filter(([key]) => key !== "defwidget@ai")); + const widgets = sortByDisplayOrder(filteredWidgets); const checkModeNeeded = useCallback(() => { if (!containerRef.current || !measurementRef.current) return; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 52a5f50d34..e816e548eb 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -14,6 +14,7 @@ declare global { workspace: jotai.Atom; // driven from WOS fullConfigAtom: jotai.PrimitiveAtom; // driven from WOS, settings -- updated via WebSocket settingsAtom: jotai.Atom; // derrived from fullConfig + hasCustomAIPresetsAtom: jotai.Atom; // derived from fullConfig tabAtom: jotai.Atom; // driven from WOS staticTabId: jotai.Atom; isFullScreen: jotai.PrimitiveAtom; diff --git a/package-lock.json b/package-lock.json index 74aec0396c..54a16f19ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.0-beta.2", + "version": "0.12.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.0-beta.2", + "version": "0.12.0-beta.3", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/aiusechat/openai/openai-backend.go b/pkg/aiusechat/openai/openai-backend.go index 6f4a4301f2..d667234227 100644 --- a/pkg/aiusechat/openai/openai-backend.go +++ b/pkg/aiusechat/openai/openai-backend.go @@ -20,6 +20,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/util/utilfn" + "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/web/sse" ) @@ -813,7 +814,7 @@ func handleOpenAIEvent( // _ = sse.AiMsgToolInputAvailable(st.toolCallID, st.toolName, raw) toolDef := state.chatOpts.GetToolDefinition(st.toolName) - toolUseData := createToolUseData(st.toolCallID, st.toolName, toolDef, ev.Arguments) + toolUseData := createToolUseData(st.toolCallID, st.toolName, toolDef, ev.Arguments, state.chatOpts) state.toolUseData[st.toolCallID] = toolUseData if toolUseData.Approval == uctypes.ApprovalNeedsApproval && state.chatOpts.RegisterToolApproval != nil { state.chatOpts.RegisterToolApproval(st.toolCallID) @@ -840,7 +841,8 @@ func handleOpenAIEvent( return nil, nil } } -func createToolUseData(toolCallID, toolName string, toolDef *uctypes.ToolDefinition, arguments string) *uctypes.UIMessageDataToolUse { + +func createToolUseData(toolCallID, toolName string, toolDef *uctypes.ToolDefinition, arguments string, chatOpts uctypes.WaveChatOpts) *uctypes.UIMessageDataToolUse { toolUseData := &uctypes.UIMessageDataToolUse{ ToolCallId: toolCallID, ToolName: toolName, @@ -868,6 +870,19 @@ func createToolUseData(toolCallID, toolName string, toolDef *uctypes.ToolDefinit toolUseData.Approval = toolDef.ToolApproval(parsedArgs) } + if chatOpts.TabId != "" { + if argsMap, ok := parsedArgs.(map[string]any); ok { + if widgetId, ok := argsMap["widget_id"].(string); ok && widgetId != "" { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, chatOpts.TabId, widgetId) + if err == nil { + toolUseData.BlockId = fullBlockId + } + } + } + } + return toolUseData } diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 8b9ba3f82b..091d149806 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -15,20 +15,6 @@ import ( "github.com/wavetermdev/waveterm/pkg/wstore" ) -func resolveBlockIdFromPrefix(tab *waveobj.Tab, blockIdPrefix string) (string, error) { - if len(blockIdPrefix) != 8 { - return "", fmt.Errorf("widget_id must be 8 characters") - } - - for _, blockId := range tab.BlockIds { - if strings.HasPrefix(blockId, blockIdPrefix) { - return blockId, nil - } - } - - return "", fmt.Errorf("widget_id not found: %q", blockIdPrefix) -} - func MakeBlockShortDesc(block *waveobj.Block) string { if block.Meta == nil { return "" diff --git a/pkg/aiusechat/tools_screenshot.go b/pkg/aiusechat/tools_screenshot.go index ad366cd20e..5b4007933f 100644 --- a/pkg/aiusechat/tools_screenshot.go +++ b/pkg/aiusechat/tools_screenshot.go @@ -9,11 +9,10 @@ import ( "time" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" - "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshutil" - "github.com/wavetermdev/waveterm/pkg/wstore" ) func makeTabCaptureBlockScreenshot(tabId string) func(any) (string, error) { @@ -31,12 +30,7 @@ func makeTabCaptureBlockScreenshot(tabId string) func(any) (string, error) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return "", fmt.Errorf("error getting tab: %w", err) - } - - fullBlockId, err := resolveBlockIdFromPrefix(tab, blockIdPrefix) + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, blockIdPrefix) if err != nil { return "", err } diff --git a/pkg/aiusechat/tools_term.go b/pkg/aiusechat/tools_term.go index 5b400be3f8..ad28bcc895 100644 --- a/pkg/aiusechat/tools_term.go +++ b/pkg/aiusechat/tools_term.go @@ -11,11 +11,10 @@ import ( "time" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" - "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshutil" - "github.com/wavetermdev/waveterm/pkg/wstore" ) type TermGetScrollbackToolInput struct { @@ -121,12 +120,7 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return nil, fmt.Errorf("error getting tab: %w", err) - } - - fullBlockId, err := resolveBlockIdFromPrefix(tab, parsed.WidgetId) + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, parsed.WidgetId) if err != nil { return nil, err } diff --git a/pkg/aiusechat/tools_web.go b/pkg/aiusechat/tools_web.go index 80aa9d51f5..3ead729ccd 100644 --- a/pkg/aiusechat/tools_web.go +++ b/pkg/aiusechat/tools_web.go @@ -86,12 +86,7 @@ func GetWebNavigateToolDefinition(tabId string) uctypes.ToolDefinition { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) - if err != nil { - return nil, fmt.Errorf("error getting tab: %w", err) - } - - fullBlockId, err := resolveBlockIdFromPrefix(tab, parsed.WidgetId) + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, parsed.WidgetId) if err != nil { return nil, err } diff --git a/pkg/aiusechat/uctypes/usechat-types.go b/pkg/aiusechat/uctypes/usechat-types.go index a8890e243c..7f478e945e 100644 --- a/pkg/aiusechat/uctypes/usechat-types.go +++ b/pkg/aiusechat/uctypes/usechat-types.go @@ -140,6 +140,7 @@ type UIMessageDataToolUse struct { Status string `json:"status"` ErrorMessage string `json:"errormessage,omitempty"` Approval string `json:"approval,omitempty"` + BlockId string `json:"blockid,omitempty"` } func (d *UIMessageDataToolUse) IsApproved() bool { @@ -422,14 +423,15 @@ type WaveChatOpts struct { Config AIOptsType Tools []ToolDefinition SystemPrompt []string - TabStateGenerator func() (string, []ToolDefinition, error) + TabStateGenerator func() (string, []ToolDefinition, string, error) WidgetAccess bool RegisterToolApproval func(string) AllowNativeWebSearch bool - // emphemeral to the step + // ephemeral to the step TabState string TabTools []ToolDefinition + TabId string } func (opts *WaveChatOpts) GetToolDefinition(toolName string) *ToolDefinition { diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index a7e54e27a9..7a661f10bf 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -362,10 +362,11 @@ func RunAIChat(ctx context.Context, sseHandler *sse.SSEHandlerCh, chatOpts uctyp var cont *uctypes.WaveContinueResponse for { if chatOpts.TabStateGenerator != nil { - tabState, tabTools, tabErr := chatOpts.TabStateGenerator() + tabState, tabTools, tabId, tabErr := chatOpts.TabStateGenerator() if tabErr == nil { chatOpts.TabState = tabState chatOpts.TabTools = tabTools + chatOpts.TabId = tabId } } stopReason, rtnMessage, err := runAIChatStep(ctx, sseHandler, chatOpts, cont) @@ -621,8 +622,9 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) { chatOpts.SystemPrompt = []string{SystemPromptText} } - chatOpts.TabStateGenerator = func() (string, []uctypes.ToolDefinition, error) { - return GenerateTabStateAndTools(r.Context(), req.TabId, req.WidgetAccess) + chatOpts.TabStateGenerator = func() (string, []uctypes.ToolDefinition, string, error) { + tabState, tabTools, err := GenerateTabStateAndTools(r.Context(), req.TabId, req.WidgetAccess) + return tabState, tabTools, req.TabId, err } // Validate the message diff --git a/pkg/wcore/wcore.go b/pkg/wcore/wcore.go index 04f1c873e3..812a2d0a42 100644 --- a/pkg/wcore/wcore.go +++ b/pkg/wcore/wcore.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "log" + "strings" "time" "github.com/google/uuid" @@ -108,3 +109,23 @@ func SendWaveObjUpdate(oref waveobj.ORef) { }, }) } + + +func ResolveBlockIdFromPrefix(ctx context.Context, tabId string, blockIdPrefix string) (string, error) { + if len(blockIdPrefix) != 8 { + return "", fmt.Errorf("widget_id must be 8 characters") + } + + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return "", fmt.Errorf("error getting tab: %w", err) + } + + for _, blockId := range tab.BlockIds { + if strings.HasPrefix(blockId, blockIdPrefix) { + return blockId, nil + } + } + + return "", fmt.Errorf("widget_id not found: %q", blockIdPrefix) +}