{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)
+}